mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-01 15:30:52 +08:00
Release v1.118.0
This commit is contained in:
@@ -69,6 +69,8 @@ internal/exec-plans/*
|
||||
!internal/exec-plans/right-surface-implementation-progress.md
|
||||
!internal/exec-plans/agent-app-host-v3-implementation.md
|
||||
!internal/exec-plans/windows-runtime-model-route-repair-plan.md
|
||||
!internal/exec-plans/windows-model-catalog-and-updater-reliability.md
|
||||
!internal/exec-plans/core-user-flow-e2e-audit-2026-07-31.md
|
||||
!internal/exec-plans/agent-app-uninstall-current-ui-progress.md
|
||||
!internal/exec-plans/writing-v2-workflow-completion-plan.md
|
||||
!internal/exec-plans/workflow-standardization-plan.md
|
||||
|
||||
+22
-20
@@ -1,41 +1,43 @@
|
||||
## Lime v1.117.0
|
||||
## Lime v1.118.0
|
||||
|
||||
<sub>The Simplified Chinese release notes are the primary version. This English page is a companion for international readers.</sub>
|
||||
|
||||
### New Features
|
||||
|
||||
- Completed the `currentTime/read`, `item/permissions/requestApproval`, and `item/tool/call` host capabilities, keeping clock reads, permission escalation, and desktop dynamic-tool calls on the same Electron Host, App Server JSON-RPC, and RuntimeCore product chain.
|
||||
- Promoted dynamic-tool calls to canonical Thread Items with namespace, tool, raw arguments, ordered text/image/audio output, status, and duration, preserving the lifecycle across session restoration.
|
||||
- Completed the six AgentControl tools, typed wait states, child-agent activity projection, and cold-restart restoration for Multi-Agent conversations; parent-owned child threads now explicitly reject direct input.
|
||||
- Added canonical execution recovery for known Lime Hub models through the bundled registry and canonical Agnes 2.5 Flash/Pro catalog entries, while unknown models remain fail-closed.
|
||||
- Skill catalogs now refresh automatically when Skills are created, changed, or removed under default roots, or after successful local Lime Skill mutations. Composer always reloads the current App Server `skill/list`, without requiring a manual refresh or app restart.
|
||||
- Agent plans now use the typed `turn/plan/updated` notification, showing the same checklist during a live Turn and after cold restoration. Failed or malformed snapshots cannot replace the latest valid plan.
|
||||
- Agent errors now follow a typed retry and terminal chain: retrying, final failure, and the authoritative Turn terminal have distinct states and can be restored from the canonical read model.
|
||||
- MCP OAuth completion now reaches the Renderer through a direct App Server v2 typed notification, keeping the login window, MCP management state, and event identity aligned.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fixed clean-environment resolution of `@limecloud/app-server-client/browser`, image-task fixtures using an invalid Chat model, model-list generation baseline drift, and incorrect JSON-RPC codes for invalid model parameters.
|
||||
- Fixed model selectors exposing inferred-only models that RuntimeCore cannot execute, live provider metadata being overwritten by local hints, and persisted Windows Lime Hub models failing on the first Turn.
|
||||
- Fixed media previews depending on transient chunk/completed notifications and Renderer live-drain helpers; every ranged response now validates offset, length, digest, and size before advancing progress.
|
||||
- Fixed boundary cases in initial-session and child-agent navigation, canonical Item reads, event sequencing, and pending-interaction restoration.
|
||||
- Fixed explicitly configured Provider models on Windows being hidden from the chat model selector when authoritative capability metadata was unavailable. Inferred-only, non-executable models remain fail closed.
|
||||
- Fixed Electron updater sessions treating equal, older, invalid, or missing versions as installable updates. Only a strictly newer semantic version can enter download and installation.
|
||||
- Fixed update failures, up-to-date results, and available updates sharing the same message, and aligned download, retry, and restart states across the sidebar, About page, and update notification window.
|
||||
- Fixed inconsistent presentation of non-executable Provider routes between submit failures and runtime events, and missing global toast hosts on standalone pages.
|
||||
- Fixed the Provider setup flow showing “Finish adding” before a configuration had been persisted and returning to the wrong model context after a successful save.
|
||||
|
||||
### Improvements and Refactors
|
||||
|
||||
- Migrated `configWarning` from v0 DTOs and schemas to one typed v2 owner; the Renderer only projects decoded global warnings and shows deduplicated messages across all five locales.
|
||||
- Removed the v0 config-warning and TextPosition/TextRange schemas, media transient-notification consumers, the old protocol facade, and duplicate preview synchronization modules without production compatibility wrappers.
|
||||
- Tightened permission-profile, sandbox-command, dynamic-tool routing, provider-history, and read-model boundaries; reserved names, schema collisions, identity mismatches, duplicate responses, and late responses all fail closed.
|
||||
- Switched Skill installation locations to the authoritative backend `localDirectoryPath`, using Electron `openPath` for directories and `showItemInFolder` for files.
|
||||
- Consolidated `skills/changed`, `error`, `turn/plan/updated`, and `mcpServer/oauthLogin/completed` into the App Server v2 protocol, schemas, generated client, and Renderer typed event bus without adding a production mock or second event owner.
|
||||
- Unified Skill catalog cache invalidation in `lime-skills`; default roots use recursive watchers and throttled notifications, while reconnect and remount still perform an active current-catalog read.
|
||||
- Kept the canonical `update_plan` tool item as the restoration source without generating a duplicate Plan Item, tool card, or decision surface.
|
||||
- Moved updater version comparison and session transitions into a focused pure owner and tightened the `available -> downloaded -> installing/restarting` state flow.
|
||||
|
||||
### Tests and Quality
|
||||
|
||||
- Added Rust, Electron, protocol, and Renderer regressions for host capabilities, dynamic tools, permission profiles, current time, AgentControl, parent-owned threads, v2 config warnings, and model routing.
|
||||
- Added real Electron Gate B evidence covering preload/IPC, `app_server_handle_json_lines`, App Server, RuntimeCore, canonical read models, visible GUI state, and cold-restart identity.
|
||||
- Strengthened guards for the app-server-client browser subpath, generated protocols, five-locale resources, legacy surfaces, and the current bridge.
|
||||
- Added Rust, Electron, protocol, Renderer, and script regressions for Skills refresh, typed errors, plan updates, MCP OAuth, Provider model catalogs, and updater semantics.
|
||||
- Expanded the current Agent fixture with typed-error success/failure, live and restored plan updates, Skills runtime refresh, and real Electron/preload/IPC/App Server/read-model identity checks.
|
||||
- Audited the core user flow across startup, Provider/model selection, Agent chat, stop-and-continue, history restoration, About, and updater states, with zero production mock fallbacks.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Updated architecture sources for host capabilities, the canonical DynamicTool payload, v2 config warnings, media transient retirement, and Lime Hub capability provenance.
|
||||
- Updated Refactor v2 V2-04/V2-05 progress, Gate B evidence, the Windows runtime model-routing plan, and the Soul output-surface roadmap.
|
||||
- Updated architecture and execution sources for App Server v2 notifications, Skill catalog invalidation, MCP OAuth, typed error/plan recovery, and updater/model-catalog behavior.
|
||||
- Recorded the Windows model-catalog and updater reliability work, core user-flow E2E audit, and the current Refactor v2 V2-05 status.
|
||||
|
||||
### Other
|
||||
|
||||
- Bumped the root app, CLI npm package, Rust workspace, and lockfile versions to `1.117.0`.
|
||||
- Bumped the root app, CLI npm package, Rust workspace, and lockfile versions to `1.118.0`.
|
||||
- The Windows Squirrel packaged Gate B for N-1 discovery, download, restart, and installation still requires a real Windows host; macOS Electron evidence does not replace that platform check.
|
||||
|
||||
**Full changes**: `v1.116.0` -> `v1.117.0`
|
||||
**Full changes**: `v1.117.0` -> `v1.118.0`
|
||||
|
||||
+22
-20
@@ -1,39 +1,41 @@
|
||||
## Lime v1.117.0
|
||||
## Lime v1.118.0
|
||||
|
||||
### 新功能
|
||||
|
||||
- 完成 `currentTime/read`、`item/permissions/requestApproval` 与 `item/tool/call` 三项 Host capability:系统时间读取、权限提升和桌面动态工具调用都经 Electron Host、App Server JSON-RPC 与 RuntimeCore 的同一条产品链闭环。
|
||||
- 动态工具调用升级为 canonical Thread Item,完整保留 namespace、tool、原始参数、有序文本/图片/音频输出、状态和耗时;恢复会话后仍能可靠呈现工具生命周期。
|
||||
- Multi-Agent 对话补齐六项 AgentControl 工具、typed wait state、子 Agent 活动投影和冷重启恢复;parent-owned 子线程现在明确禁止直接输入。
|
||||
- Lime Hub 可通过 bundled canonical registry 恢复已知模型的执行能力,并新增 Agnes 2.5 Flash/Pro canonical catalog;未知模型仍保持 fail-closed。
|
||||
- Skill 目录现在会在默认目录新增、修改或删除 Skill,以及 Lime 本地 Skill 管理成功后自动刷新;Composer 始终重新读取 App Server 的 current `skill/list`,无需手动刷新或重启应用。
|
||||
- Agent 执行计划接入 typed `turn/plan/updated` 通知,实时对话与冷恢复会展示同一份 checklist;失败或畸形的计划快照不会覆盖最近一次有效状态。
|
||||
- Agent 错误升级为 typed retry/terminal 链路:重试中、最终失败与权威 Turn terminal 各自有明确状态,并可从 canonical read model 恢复。
|
||||
- MCP OAuth 登录完成态改由 App Server v2 typed notification 直接投影到 Renderer,统一登录窗口、MCP 管理状态与事件 identity。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复干净环境无法解析 `@limecloud/app-server-client/browser`、图片任务 fixture 使用错误 Chat 模型、模型列表 generation 基线漂移,以及非法模型参数返回错误 JSON-RPC code 的问题。
|
||||
- 修复模型选择器展示 RuntimeCore 无法执行的 inferred-only 模型、live provider metadata 被本地 hint 覆盖,以及 Windows Lime Hub 已保存模型无法进入首回合的问题。
|
||||
- 修复媒体预览依赖 transient chunk/completed 通知和 Renderer live-drain helper 的双轨行为;所有分段读取现在先校验 offset、length、digest 与大小再更新进度。
|
||||
- 修复初始会话和子 Agent 导航、canonical Item 读取、事件序列门控与待处理交互恢复中的边界问题。
|
||||
- 修复 Windows 用户已显式配置的 Provider 模型因缺少权威 capability 来源而不出现在聊天模型选择器中的问题;仅靠启发式推断且不可执行的模型继续 fail closed。
|
||||
- 修复 Electron updater 将同版本、旧版本、无效版本或缺失版本误报为可安装更新的问题;只有严格更高的语义版本进入下载和安装流程。
|
||||
- 修复更新失败、已是最新和发现新版本共用提示的问题,并统一侧边栏、关于页与更新通知页的下载、重试和重启安装状态。
|
||||
- 修复 Provider 路由不可执行错误在发送入口和 runtime 事件链中出现不一致提示,以及部分独立页面缺少全局 toast 宿主的问题。
|
||||
- 修复 Provider 添加流程在尚未保存配置时显示“完成添加”,并在已保存后正确返回可用模型上下文。
|
||||
|
||||
### 优化与重构
|
||||
|
||||
- 将 `configWarning` 从 v0 DTO/schema 迁移到唯一的 typed v2 owner,Renderer 只投影解码后的全局告警,并提供五语言去重提示。
|
||||
- 删除 v0 配置告警、TextPosition/TextRange schema、媒体 transient notification 消费者、旧协议 facade 与重复预览同步模块,不保留生产兼容包装。
|
||||
- 收紧请求权限 profile、沙箱命令、动态工具路由、provider history 与 read model 的类型边界;保留名、schema 冲突、身份错配、重复或迟到响应全部 fail-closed。
|
||||
- Skill 安装目录改用后端权威 `localDirectoryPath`,Electron 对目录使用 `openPath`、对文件使用 `showItemInFolder`。
|
||||
- 将 `skills/changed`、`error`、`turn/plan/updated` 与 `mcpServer/oauthLogin/completed` 收敛到 App Server v2 protocol、schema、generated client 和 Renderer typed event bus,不新增生产 mock 或第二事件 owner。
|
||||
- Skill catalog cache 统一由 `lime-skills` 失效;默认 Skill roots 使用递归 watcher 与节流通知,重连和 remount 仍主动读取 current catalog。
|
||||
- canonical `update_plan` 工具项继续作为恢复事实源,但不再生成重复 Plan Item、工具卡或决策面板。
|
||||
- updater 的版本比较与会话状态拆到独立纯 owner,收紧 `available -> downloaded -> installing/restarting` 状态转换。
|
||||
|
||||
### 测试与质量
|
||||
|
||||
- 新增 Host capability、动态工具、权限 profile、current time、AgentControl、parent-owned thread、配置告警 v2 与模型路由的 Rust、Electron、协议和 Renderer 回归。
|
||||
- 补充真实 Electron Gate B 证据,覆盖 preload/IPC、`app_server_handle_json_lines`、App Server、RuntimeCore、canonical read model、GUI 可见状态和冷重启 identity。
|
||||
- 加强 app-server-client browser 子路径、协议生成物、五语言资源、legacy surface 和 current bridge 的守卫。
|
||||
- 新增 Skills 自动刷新、typed error、执行计划、MCP OAuth、Provider 模型目录与 updater 语义的 Rust、Electron、协议、Renderer 和脚本回归。
|
||||
- 扩展 current Agent fixture,覆盖 typed error 成功/失败、计划实时更新与冷恢复、Skills runtime 自动刷新,以及真实 Electron/preload/IPC/App Server/read model identity。
|
||||
- 核心用户流程审计覆盖启动、Provider/模型选择、Agent 对话、停止后继续、历史恢复、关于页与更新状态;生产 mock fallback 命中为零。
|
||||
|
||||
### 文档
|
||||
|
||||
- 更新 Host capability、DynamicTool canonical payload、配置告警 v2、媒体 transient retirement 与 Lime Hub capability provenance 的架构事实源。
|
||||
- 更新 Refactor v2 V2-04/V2-05 进度、Gate B evidence、Windows runtime 模型路由计划和 Soul 输出面路线图。
|
||||
- 更新 App Server v2 notification、Skills catalog invalidation、MCP OAuth、typed error/plan recovery 与 updater/模型目录的架构和执行事实源。
|
||||
- 记录 Windows 模型目录与更新可靠性、核心用户主流程 E2E 审计,以及 Refactor v2 V2-05 当前完成情况。
|
||||
|
||||
### 其他
|
||||
|
||||
- 将根应用、CLI npm 包、Rust workspace 与锁文件版本统一提升到 `1.117.0`。
|
||||
- 将根应用、CLI npm 包、Rust workspace 与锁文件版本统一提升到 `1.118.0`。
|
||||
- Windows Squirrel 从 N-1 版本发现、下载、重启安装的 packaged Gate B 仍需 Windows 实机验收;macOS Electron 证据不替代该平台验证。
|
||||
|
||||
**完整变更**: `v1.116.0` -> `v1.117.0`
|
||||
**完整变更**: `v1.117.0` -> `v1.118.0`
|
||||
|
||||
+101
-31
@@ -74,6 +74,17 @@ vi.mock("./electronRuntime", () => ({
|
||||
|
||||
import { ElectronUpdateHost } from "./updateHost";
|
||||
|
||||
function emitDownloadedUpdate(releaseName: string | undefined): void {
|
||||
autoUpdaterEmit(
|
||||
"update-downloaded",
|
||||
{},
|
||||
"release notes",
|
||||
releaseName,
|
||||
new Date("2026-07-17T00:00:00.000Z"),
|
||||
"https://updates.limecloud.com/Lime-update.zip",
|
||||
);
|
||||
}
|
||||
|
||||
describe("ElectronUpdateHost", () => {
|
||||
beforeEach(() => {
|
||||
appState.isPackaged = false;
|
||||
@@ -196,22 +207,18 @@ describe("ElectronUpdateHost", () => {
|
||||
});
|
||||
const host = new ElectronUpdateHost(vi.fn());
|
||||
|
||||
await expect(host.invoke("check_for_updates")).resolves.toEqual(
|
||||
expect.objectContaining({ hasUpdate: true }),
|
||||
);
|
||||
const checkSession = host.invoke("check_for_updates");
|
||||
await Promise.resolve();
|
||||
expect(checkForUpdatesMock).toHaveBeenCalledTimes(1);
|
||||
await expect(host.invoke("get_update_install_session")).resolves.toEqual(
|
||||
expect.objectContaining({ stage: "downloading" }),
|
||||
);
|
||||
|
||||
const installSession = host.invoke("start_update_install_session");
|
||||
autoUpdaterEmit(
|
||||
"update-downloaded",
|
||||
{},
|
||||
"release notes",
|
||||
"1.61.0",
|
||||
new Date("2026-07-17T00:00:00.000Z"),
|
||||
"https://updates.limecloud.com/Lime-1.61.0.zip",
|
||||
emitDownloadedUpdate("1.61.0");
|
||||
|
||||
await expect(checkSession).resolves.toEqual(
|
||||
expect.objectContaining({ hasUpdate: true, latest: "1.61.0" }),
|
||||
);
|
||||
|
||||
await expect(installSession).resolves.toEqual(
|
||||
@@ -236,12 +243,14 @@ describe("ElectronUpdateHost", () => {
|
||||
});
|
||||
const host = new ElectronUpdateHost(vi.fn());
|
||||
|
||||
await expect(
|
||||
host.invoke("check_for_updates", { automatic: true }),
|
||||
).resolves.toEqual(expect.objectContaining({ hasUpdate: true }));
|
||||
await expect(
|
||||
host.invoke("check_for_updates", { automatic: true }),
|
||||
).resolves.toEqual(expect.objectContaining({ hasUpdate: true }));
|
||||
const firstCheck = host.invoke("check_for_updates", { automatic: true });
|
||||
const secondCheck = host.invoke("check_for_updates", { automatic: true });
|
||||
emitDownloadedUpdate("1.61.0");
|
||||
|
||||
await expect(Promise.all([firstCheck, secondCheck])).resolves.toEqual([
|
||||
expect.objectContaining({ hasUpdate: true }),
|
||||
expect.objectContaining({ hasUpdate: true }),
|
||||
]);
|
||||
|
||||
expect(checkForUpdatesMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -255,28 +264,89 @@ describe("ElectronUpdateHost", () => {
|
||||
});
|
||||
const host = new ElectronUpdateHost(vi.fn());
|
||||
|
||||
await host.invoke("check_for_updates");
|
||||
const checkSession = host.invoke("check_for_updates");
|
||||
const firstInstall = host.invoke("start_update_install_session");
|
||||
const secondInstall = host.invoke("start_update_install_session");
|
||||
autoUpdaterEmit(
|
||||
"update-downloaded",
|
||||
{},
|
||||
"release notes",
|
||||
"1.61.0",
|
||||
new Date("2026-07-17T00:00:00.000Z"),
|
||||
"https://updates.limecloud.com/Lime-1.61.0.zip",
|
||||
);
|
||||
emitDownloadedUpdate("1.61.0");
|
||||
|
||||
await expect(Promise.all([firstInstall, secondInstall])).resolves.toEqual(
|
||||
[
|
||||
expect.objectContaining({ stage: "restarting" }),
|
||||
expect.objectContaining({ stage: "restarting" }),
|
||||
],
|
||||
);
|
||||
await expect(
|
||||
Promise.all([checkSession, firstInstall, secondInstall]),
|
||||
).resolves.toEqual([
|
||||
expect.objectContaining({ hasUpdate: true }),
|
||||
expect.objectContaining({ stage: "restarting" }),
|
||||
expect.objectContaining({ stage: "restarting" }),
|
||||
]);
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(quitAndInstallMock).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["1.60.0", "v1.60.0", "1.59.0"])(
|
||||
"候选版本 %s 不高于当前版本时不得进入安装",
|
||||
async (candidateVersion) => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
appState.isPackaged = true;
|
||||
checkForUpdatesMock.mockImplementation(() => {
|
||||
autoUpdaterEmit("update-available");
|
||||
});
|
||||
const host = new ElectronUpdateHost(vi.fn());
|
||||
|
||||
const checkSession = host.invoke("check_for_updates");
|
||||
const installSession = host.invoke("start_update_install_session");
|
||||
emitDownloadedUpdate(candidateVersion);
|
||||
|
||||
await expect(checkSession).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
hasUpdate: false,
|
||||
latest: candidateVersion.replace(/^v/i, ""),
|
||||
}),
|
||||
);
|
||||
await expect(installSession).resolves.toEqual(
|
||||
expect.objectContaining({ stage: "up_to_date" }),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(quitAndInstallMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([undefined, "latest"])(
|
||||
"候选版本 %s 无效时应终止更新且不得安装",
|
||||
async (candidateVersion) => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
appState.isPackaged = true;
|
||||
checkForUpdatesMock.mockImplementation(() => {
|
||||
autoUpdaterEmit("update-available");
|
||||
});
|
||||
const host = new ElectronUpdateHost(vi.fn());
|
||||
|
||||
const checkSession = host.invoke("check_for_updates");
|
||||
const installSession = host.invoke("start_update_install_session");
|
||||
emitDownloadedUpdate(candidateVersion);
|
||||
|
||||
await expect(checkSession).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
hasUpdate: false,
|
||||
error: "更新清单未提供有效的候选版本",
|
||||
}),
|
||||
);
|
||||
await expect(installSession).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
stage: "failed",
|
||||
latestVersion: null,
|
||||
}),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
expect(quitAndInstallMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+139
-145
@@ -1,4 +1,9 @@
|
||||
import { app, autoUpdater } from "./electronRuntime";
|
||||
import {
|
||||
assessUpdateCandidate,
|
||||
normalizeUpdateVersion,
|
||||
type UpdateCandidateAssessment,
|
||||
} from "./updateVersion";
|
||||
|
||||
type UpdateInstallStage =
|
||||
| "idle"
|
||||
@@ -84,7 +89,6 @@ export class ElectronUpdateHost {
|
||||
readonly #windowController: UpdateWindowController;
|
||||
#configured = false;
|
||||
#checkPromise: Promise<UpdateInfo | null> | null = null;
|
||||
#downloadPromise: Promise<UpdateInfo> | null = null;
|
||||
#session: UpdateInstallSession;
|
||||
#latestInfo: UpdateInfo | null = null;
|
||||
|
||||
@@ -156,27 +160,40 @@ export class ElectronUpdateHost {
|
||||
};
|
||||
}
|
||||
this.#configure();
|
||||
if (this.#hasPendingUpdate()) {
|
||||
if (this.#hasConfirmedUpdate()) {
|
||||
return this.#currentVersionInfo();
|
||||
}
|
||||
|
||||
this.#setSession({
|
||||
stage: "checking",
|
||||
message: "正在检查更新",
|
||||
isActive: true,
|
||||
});
|
||||
if (!this.#checkPromise) {
|
||||
this.#setSession({
|
||||
stage: "checking",
|
||||
latestVersion: null,
|
||||
downloadUrl: null,
|
||||
error: null,
|
||||
message: "正在检查更新",
|
||||
isActive: true,
|
||||
completedAt: null,
|
||||
});
|
||||
}
|
||||
try {
|
||||
const info = await this.#checkForUpdatesOnce();
|
||||
this.#latestInfo = info;
|
||||
const hasUpdate = Boolean(info && info.version !== app.getVersion());
|
||||
const assessment = this.#assessUpdateInfo(info);
|
||||
if (assessment.status === "invalid") {
|
||||
throw new Error("更新清单未提供有效的候选版本");
|
||||
}
|
||||
const hasUpdate = assessment.status === "newer";
|
||||
this.#setSession({
|
||||
stage: hasUpdate ? "downloading" : "up_to_date",
|
||||
latestVersion: info?.version ?? null,
|
||||
message: hasUpdate ? "发现新版本,正在下载更新" : "当前已是最新版本",
|
||||
isActive: hasUpdate,
|
||||
completedAt: hasUpdate ? null : Date.now(),
|
||||
stage: hasUpdate ? "completed" : "up_to_date",
|
||||
latestVersion: assessment.version,
|
||||
downloadUrl: hasUpdate ? this.#downloadUrl(info) : null,
|
||||
percent: hasUpdate ? 1 : 0,
|
||||
error: null,
|
||||
message: hasUpdate ? "更新已下载,准备安装" : "当前已是最新版本",
|
||||
isActive: false,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
return this.#versionInfo(info, hasUpdate);
|
||||
return this.#versionInfo(info);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.#setSession({
|
||||
@@ -204,13 +221,6 @@ export class ElectronUpdateHost {
|
||||
}
|
||||
this.#configure();
|
||||
|
||||
if (this.#session.stage === "downloading") {
|
||||
return {
|
||||
success: true,
|
||||
message: "更新下载已开始",
|
||||
filePath: null,
|
||||
};
|
||||
}
|
||||
if (this.#session.stage === "completed") {
|
||||
return {
|
||||
success: true,
|
||||
@@ -226,50 +236,19 @@ export class ElectronUpdateHost {
|
||||
};
|
||||
}
|
||||
|
||||
this.#setSession({
|
||||
stage: "checking",
|
||||
latestVersion: this.#latestInfo?.version ?? null,
|
||||
message: "正在检查并下载更新",
|
||||
isActive: true,
|
||||
});
|
||||
try {
|
||||
const info = await this.#checkForUpdatesOnce();
|
||||
if (!info) {
|
||||
this.#setSession({
|
||||
stage: "up_to_date",
|
||||
message: "当前已是最新版本",
|
||||
isActive: false,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
message: "当前已是最新版本",
|
||||
filePath: null,
|
||||
};
|
||||
}
|
||||
this.#latestInfo = info;
|
||||
this.#setSession({
|
||||
stage: "downloading",
|
||||
latestVersion: info.version ?? null,
|
||||
message: "更新下载已开始",
|
||||
isActive: true,
|
||||
});
|
||||
const versionInfo = await this.checkForUpdates();
|
||||
if (versionInfo.hasUpdate) {
|
||||
return {
|
||||
success: true,
|
||||
message: "更新下载已开始",
|
||||
message: "更新已下载,准备安装",
|
||||
filePath: null,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.#setSession({
|
||||
stage: "failed",
|
||||
error: message,
|
||||
message,
|
||||
isActive: false,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
return { success: false, message };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: versionInfo.error || "当前已是最新版本",
|
||||
filePath: null,
|
||||
};
|
||||
}
|
||||
|
||||
async startInstallSession(): Promise<UpdateInstallSession> {
|
||||
@@ -292,30 +271,48 @@ export class ElectronUpdateHost {
|
||||
return this.#session;
|
||||
}
|
||||
if (this.#session.stage !== "completed") {
|
||||
if (this.#session.stage !== "downloading") {
|
||||
if (!this.#checkPromise) {
|
||||
this.#setSession({
|
||||
stage: "checking",
|
||||
latestVersion: null,
|
||||
downloadUrl: null,
|
||||
error: null,
|
||||
message: "正在检查并下载更新",
|
||||
isActive: true,
|
||||
completedAt: null,
|
||||
});
|
||||
const info = await this.#checkForUpdatesOnce();
|
||||
if (!info) {
|
||||
this.#setSession({
|
||||
stage: "up_to_date",
|
||||
message: "当前已是最新版本",
|
||||
isActive: false,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
return this.#session;
|
||||
}
|
||||
}
|
||||
await this.#waitForDownloadedUpdate();
|
||||
const info = await this.#checkForUpdatesOnce();
|
||||
this.#latestInfo = info;
|
||||
const assessment = this.#assessUpdateInfo(info);
|
||||
if (assessment.status === "invalid") {
|
||||
throw new Error("更新清单未提供有效的候选版本");
|
||||
}
|
||||
if (assessment.status !== "newer") {
|
||||
this.#setSession({
|
||||
stage: "up_to_date",
|
||||
latestVersion: assessment.version,
|
||||
downloadUrl: null,
|
||||
percent: 0,
|
||||
error: null,
|
||||
message: "当前已是最新版本",
|
||||
isActive: false,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
return this.#session;
|
||||
}
|
||||
}
|
||||
if (this.#isInstallingUpdate()) {
|
||||
return this.#session;
|
||||
}
|
||||
const installAssessment = this.#assessUpdateInfo(this.#latestInfo);
|
||||
if (installAssessment.status !== "newer") {
|
||||
throw new Error("更新版本未通过安装前校验");
|
||||
}
|
||||
this.#setSession({
|
||||
stage: "restarting",
|
||||
latestVersion: installAssessment.version,
|
||||
error: null,
|
||||
message: "正在重启并安装更新",
|
||||
isActive: true,
|
||||
canCloseWindow: false,
|
||||
@@ -350,18 +347,25 @@ export class ElectronUpdateHost {
|
||||
),
|
||||
});
|
||||
autoUpdater.on("update-available", () => {
|
||||
this.#latestInfo = {};
|
||||
this.#latestInfo = null;
|
||||
this.#setSession({
|
||||
stage: "downloading",
|
||||
latestVersion: null,
|
||||
downloadUrl: null,
|
||||
message: "发现新版本,正在下载更新",
|
||||
error: null,
|
||||
message: "正在下载并验证更新",
|
||||
isActive: true,
|
||||
completedAt: null,
|
||||
});
|
||||
});
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
this.#latestInfo = null;
|
||||
this.#setSession({
|
||||
stage: "up_to_date",
|
||||
latestVersion: null,
|
||||
downloadUrl: null,
|
||||
percent: 0,
|
||||
error: null,
|
||||
message: "当前已是最新版本",
|
||||
isActive: false,
|
||||
completedAt: Date.now(),
|
||||
@@ -376,16 +380,7 @@ export class ElectronUpdateHost {
|
||||
releaseDate,
|
||||
updateURL,
|
||||
);
|
||||
this.#latestInfo = info;
|
||||
this.#setSession({
|
||||
stage: "completed",
|
||||
latestVersion: info.version ?? null,
|
||||
downloadUrl: updateURL || null,
|
||||
percent: 1,
|
||||
message: "更新已下载,准备安装",
|
||||
isActive: false,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
this.#applyDownloadedUpdate(info);
|
||||
},
|
||||
);
|
||||
autoUpdater.on("error", (error) => {
|
||||
@@ -419,12 +414,14 @@ export class ElectronUpdateHost {
|
||||
return app.isPackaged;
|
||||
}
|
||||
|
||||
#versionInfo(info: UpdateInfo | null, hasUpdate: boolean): VersionInfo {
|
||||
#versionInfo(info: UpdateInfo | null): VersionInfo {
|
||||
const assessment = this.#assessUpdateInfo(info);
|
||||
const hasUpdate = assessment.status === "newer";
|
||||
return {
|
||||
current: app.getVersion(),
|
||||
latest: info?.version ?? null,
|
||||
latest: assessment.version,
|
||||
hasUpdate,
|
||||
downloadUrl: this.#downloadUrl(info),
|
||||
downloadUrl: hasUpdate ? this.#downloadUrl(info) : null,
|
||||
releaseNotes:
|
||||
typeof info?.releaseNotes === "string" ? info.releaseNotes : null,
|
||||
releaseNotesUrl: null,
|
||||
@@ -434,22 +431,15 @@ export class ElectronUpdateHost {
|
||||
}
|
||||
|
||||
#currentVersionInfo(): VersionInfo {
|
||||
const hasUpdate = Boolean(
|
||||
this.#latestInfo && this.#latestInfo.version !== app.getVersion(),
|
||||
);
|
||||
return this.#versionInfo(this.#latestInfo, hasUpdate);
|
||||
return this.#versionInfo(this.#latestInfo);
|
||||
}
|
||||
|
||||
#downloadUrl(info: UpdateInfo | null): string | null {
|
||||
return info?.updateURL ?? null;
|
||||
}
|
||||
|
||||
#hasPendingUpdate(): boolean {
|
||||
return (
|
||||
this.#session.stage === "downloading" ||
|
||||
this.#session.stage === "completed" ||
|
||||
this.#isInstallingUpdate()
|
||||
);
|
||||
#hasConfirmedUpdate(): boolean {
|
||||
return this.#session.stage === "completed" || this.#isInstallingUpdate();
|
||||
}
|
||||
|
||||
#isInstallingUpdate(): boolean {
|
||||
@@ -464,48 +454,6 @@ export class ElectronUpdateHost {
|
||||
return await this.#checkPromise;
|
||||
}
|
||||
const promise = new Promise<UpdateInfo | null>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
autoUpdater.removeListener("update-available", onAvailable);
|
||||
autoUpdater.removeListener("update-not-available", onNotAvailable);
|
||||
autoUpdater.removeListener("error", onError);
|
||||
};
|
||||
const onAvailable = () => {
|
||||
cleanup();
|
||||
resolve(this.#latestInfo ?? {});
|
||||
};
|
||||
const onNotAvailable = () => {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
};
|
||||
const onError = (error: Error) => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
};
|
||||
autoUpdater.once("update-available", onAvailable);
|
||||
autoUpdater.once("update-not-available", onNotAvailable);
|
||||
autoUpdater.once("error", onError);
|
||||
try {
|
||||
autoUpdater.checkForUpdates();
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
reject(error);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.#checkPromise = null;
|
||||
});
|
||||
this.#checkPromise = promise;
|
||||
return await promise;
|
||||
}
|
||||
|
||||
async #waitForDownloadedUpdate(): Promise<UpdateInfo> {
|
||||
if (this.#session.stage === "completed" && this.#latestInfo) {
|
||||
return this.#latestInfo;
|
||||
}
|
||||
if (this.#downloadPromise) {
|
||||
return await this.#downloadPromise;
|
||||
}
|
||||
|
||||
const promise = new Promise<UpdateInfo>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
autoUpdater.removeListener("update-downloaded", onDownloaded);
|
||||
autoUpdater.removeListener("update-not-available", onNotAvailable);
|
||||
@@ -525,28 +473,74 @@ export class ElectronUpdateHost {
|
||||
releaseDate,
|
||||
updateURL,
|
||||
);
|
||||
this.#latestInfo = info;
|
||||
if (this.#assessUpdateInfo(info).status === "invalid") {
|
||||
reject(new Error("更新清单未提供有效的候选版本"));
|
||||
return;
|
||||
}
|
||||
resolve(info);
|
||||
};
|
||||
const onNotAvailable = () => {
|
||||
cleanup();
|
||||
reject(new Error("当前已是最新版本"));
|
||||
resolve(null);
|
||||
};
|
||||
const onError = (error: Error) => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
};
|
||||
|
||||
autoUpdater.once("update-downloaded", onDownloaded);
|
||||
autoUpdater.once("update-not-available", onNotAvailable);
|
||||
autoUpdater.once("error", onError);
|
||||
try {
|
||||
autoUpdater.checkForUpdates();
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
reject(error);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.#downloadPromise = null;
|
||||
this.#checkPromise = null;
|
||||
});
|
||||
this.#downloadPromise = promise;
|
||||
this.#checkPromise = promise;
|
||||
return await promise;
|
||||
}
|
||||
|
||||
#assessUpdateInfo(info: UpdateInfo | null): UpdateCandidateAssessment {
|
||||
if (!info) {
|
||||
return { status: "not_newer", version: null };
|
||||
}
|
||||
return assessUpdateCandidate(info.version, app.getVersion());
|
||||
}
|
||||
|
||||
#applyDownloadedUpdate(info: UpdateInfo): void {
|
||||
const assessment = this.#assessUpdateInfo(info);
|
||||
if (assessment.status === "invalid") {
|
||||
this.#latestInfo = null;
|
||||
this.#setSession({
|
||||
stage: "failed",
|
||||
latestVersion: null,
|
||||
downloadUrl: null,
|
||||
percent: 0,
|
||||
error: "更新清单未提供有效的候选版本",
|
||||
message: "更新清单未提供有效的候选版本",
|
||||
isActive: false,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.#latestInfo = info;
|
||||
const hasUpdate = assessment.status === "newer";
|
||||
this.#setSession({
|
||||
stage: hasUpdate ? "completed" : "up_to_date",
|
||||
latestVersion: assessment.version,
|
||||
downloadUrl: hasUpdate ? this.#downloadUrl(info) : null,
|
||||
percent: hasUpdate ? 1 : 0,
|
||||
error: null,
|
||||
message: hasUpdate ? "更新已下载,准备安装" : "当前已是最新版本",
|
||||
isActive: false,
|
||||
completedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
#setSession(update: Partial<UpdateInstallSession>): void {
|
||||
this.#session = {
|
||||
...this.#session,
|
||||
@@ -657,7 +651,7 @@ function buildUpdateInfo(
|
||||
releaseName,
|
||||
releaseNotes: typeof releaseNotes === "string" ? releaseNotes : undefined,
|
||||
updateURL,
|
||||
version: releaseName,
|
||||
version: normalizeUpdateVersion(releaseName) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { assessUpdateCandidate, normalizeUpdateVersion } from "./updateVersion";
|
||||
|
||||
describe("updateVersion", () => {
|
||||
it.each([
|
||||
["1.117.0", "1.117.0"],
|
||||
["v1.117.0", "1.117.0"],
|
||||
["1.116.0", "1.117.0"],
|
||||
])("候选版本 %s 相对当前版本 %s 不应视为更新", (candidate, current) => {
|
||||
expect(assessUpdateCandidate(candidate, current)).toEqual({
|
||||
status: "not_newer",
|
||||
version: candidate.replace(/^v/i, ""),
|
||||
});
|
||||
});
|
||||
|
||||
it("只应把严格更高版本视为可安装更新", () => {
|
||||
expect(assessUpdateCandidate("1.118.0", "1.117.0")).toEqual({
|
||||
status: "newer",
|
||||
version: "1.118.0",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([undefined, "", "latest", "1.117", "1.117.0-01"])(
|
||||
"无效候选版本 %s 应 fail closed",
|
||||
(candidate) => {
|
||||
expect(assessUpdateCandidate(candidate, "1.117.0")).toEqual({
|
||||
status: "invalid",
|
||||
version: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("应遵循 SemVer 预发布版本优先级并忽略构建元数据", () => {
|
||||
expect(assessUpdateCandidate("1.118.0-rc.1", "1.118.0-beta.9").status).toBe(
|
||||
"newer",
|
||||
);
|
||||
expect(assessUpdateCandidate("1.118.0", "1.118.0-rc.1").status).toBe(
|
||||
"newer",
|
||||
);
|
||||
expect(
|
||||
assessUpdateCandidate("1.118.0+build.2", "1.118.0+build.1").status,
|
||||
).toBe("not_newer");
|
||||
expect(normalizeUpdateVersion("V1.118.0-rc.1+build.2")).toBe(
|
||||
"1.118.0-rc.1+build.2",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
interface ParsedUpdateVersion {
|
||||
core: [string, string, string];
|
||||
normalized: string;
|
||||
prerelease: string[];
|
||||
}
|
||||
|
||||
export type UpdateCandidateStatus = "invalid" | "newer" | "not_newer";
|
||||
|
||||
export interface UpdateCandidateAssessment {
|
||||
status: UpdateCandidateStatus;
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
const UPDATE_VERSION_PATTERN =
|
||||
/^(?:v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/i;
|
||||
|
||||
function parseUpdateVersion(
|
||||
value: string | null | undefined,
|
||||
): ParsedUpdateVersion | null {
|
||||
const match = UPDATE_VERSION_PATTERN.exec(value?.trim() ?? "");
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prerelease = match[4]?.split(".") ?? [];
|
||||
if (
|
||||
prerelease.some(
|
||||
(identifier) =>
|
||||
/^\d+$/.test(identifier) &&
|
||||
identifier.length > 1 &&
|
||||
identifier.startsWith("0"),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const core: [string, string, string] = [match[1], match[2], match[3]];
|
||||
const prereleaseSuffix =
|
||||
prerelease.length > 0 ? `-${prerelease.join(".")}` : "";
|
||||
const buildSuffix = match[5] ? `+${match[5]}` : "";
|
||||
return {
|
||||
core,
|
||||
normalized: `${core.join(".")}${prereleaseSuffix}${buildSuffix}`,
|
||||
prerelease,
|
||||
};
|
||||
}
|
||||
|
||||
function compareNumericIdentifiers(left: string, right: string): number {
|
||||
if (left.length !== right.length) {
|
||||
return left.length > right.length ? 1 : -1;
|
||||
}
|
||||
if (left === right) {
|
||||
return 0;
|
||||
}
|
||||
return left > right ? 1 : -1;
|
||||
}
|
||||
|
||||
function comparePrerelease(left: string[], right: string[]): number {
|
||||
if (left.length === 0 || right.length === 0) {
|
||||
if (left.length === right.length) {
|
||||
return 0;
|
||||
}
|
||||
return left.length === 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
const length = Math.max(left.length, right.length);
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const leftIdentifier = left[index];
|
||||
const rightIdentifier = right[index];
|
||||
if (leftIdentifier === undefined || rightIdentifier === undefined) {
|
||||
return leftIdentifier === undefined ? -1 : 1;
|
||||
}
|
||||
if (leftIdentifier === rightIdentifier) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const leftNumeric = /^\d+$/.test(leftIdentifier);
|
||||
const rightNumeric = /^\d+$/.test(rightIdentifier);
|
||||
if (leftNumeric && rightNumeric) {
|
||||
return compareNumericIdentifiers(leftIdentifier, rightIdentifier);
|
||||
}
|
||||
if (leftNumeric !== rightNumeric) {
|
||||
return leftNumeric ? -1 : 1;
|
||||
}
|
||||
return leftIdentifier > rightIdentifier ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function compareParsedVersions(
|
||||
left: ParsedUpdateVersion,
|
||||
right: ParsedUpdateVersion,
|
||||
): number {
|
||||
for (let index = 0; index < left.core.length; index += 1) {
|
||||
const result = compareNumericIdentifiers(
|
||||
left.core[index],
|
||||
right.core[index],
|
||||
);
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return comparePrerelease(left.prerelease, right.prerelease);
|
||||
}
|
||||
|
||||
export function normalizeUpdateVersion(
|
||||
value: string | null | undefined,
|
||||
): string | null {
|
||||
return parseUpdateVersion(value)?.normalized ?? null;
|
||||
}
|
||||
|
||||
export function assessUpdateCandidate(
|
||||
candidateVersion: string | null | undefined,
|
||||
currentVersion: string | null | undefined,
|
||||
): UpdateCandidateAssessment {
|
||||
const candidate = parseUpdateVersion(candidateVersion);
|
||||
const current = parseUpdateVersion(currentVersion);
|
||||
if (!candidate || !current) {
|
||||
return { status: "invalid", version: null };
|
||||
}
|
||||
|
||||
return {
|
||||
status:
|
||||
compareParsedVersions(candidate, current) > 0 ? "newer" : "not_newer",
|
||||
version: candidate.normalized,
|
||||
};
|
||||
}
|
||||
@@ -1699,3 +1699,39 @@ Architecture impact: major because a live notification and recovery surface move
|
||||
to the v2 protocol/projector/Renderer boundary. The product direction remains Electron Desktop Host -> App Server
|
||||
JSON-RPC -> RuntimeCore -> Thread/Turn/Item projection -> GUI. Architecture diagram updated: this section and the App
|
||||
Server notification boundary above. Responsible developer confirmation: root, 2026-07-31.
|
||||
|
||||
## 28. Skills Changed Catalog Invalidation Owner
|
||||
|
||||
Skill catalog invalidation uses one current catalog owner and one typed transient notification path:
|
||||
|
||||
```text
|
||||
default Skill roots create / modify / remove
|
||||
or successful Lime Skill catalog mutation
|
||||
-> invalidate lime-skills executable snapshot cache
|
||||
-> App Server v2 skills/changed {}
|
||||
-> app_server_handle_json_lines notification drain
|
||||
-> Renderer typed notification bus
|
||||
-> current skill/list
|
||||
-> Composer Skill catalog projection and GUI refresh
|
||||
```
|
||||
|
||||
The App Server watches only default Skill roots. Existing roots are recursive watches; roots created after startup are
|
||||
reconciled and watched. Filesystem create, modify and remove events invalidate the executable snapshot and broadcast at
|
||||
most once per ten-second throttle window. Successful Lime catalog mutations invalidate the same cache and attach the
|
||||
same typed notification at the processor boundary; failed mutations and mutations for other apps do not notify.
|
||||
|
||||
`skill/list` is the sole executable catalog read for Composer. The Renderer API reuses the existing typed executable
|
||||
Skill decoder and applies a pure GUI projection. `skillManagement/list` retains its management-center semantics and does
|
||||
not become a second Composer catalog owner. Reconnect or remount performs a fresh list independently of notification
|
||||
delivery. `skills/changed` carries the strict empty object only and is a process-level transient invalidation; it does not
|
||||
create a Thread/Turn/Item, durable event, persistence record or replay requirement.
|
||||
|
||||
The v2 protocol/schema/generated client, App Server watcher and successful mutation producer, `lime-skills` cache
|
||||
invalidation, Renderer typed event bus and current `skill/list` refresh are `current`. No `compat` path exists.
|
||||
`skills/extraRoots/set`, a second catalog/read model, durable notification replay and production mock fallback were not
|
||||
added and are `dead / forbidden-to-restore` as alternate implementations.
|
||||
|
||||
Architecture impact: major because this adds a public notification producer and GUI invalidation boundary while keeping
|
||||
the product direction unchanged: Electron Desktop Host -> App Server JSON-RPC -> RuntimeCore/domain owner -> GUI.
|
||||
Architecture diagram updated: this section and the Skill catalog path above. Responsible developer confirmation: root,
|
||||
2026-07-31.
|
||||
|
||||
@@ -18,13 +18,13 @@ Renderer typed gateway
|
||||
|
||||
## Owner 判定
|
||||
|
||||
| 需求 | Owner |
|
||||
| --- | --- |
|
||||
| Thread / Turn / Item、read model、evidence、业务查询与写入 | App Server protocol + handler + current Rust domain |
|
||||
| 模型路由、canonical content、capability、provider wire lowering | `runtime-core` / `model-provider` |
|
||||
| 工具定义、审批、sandbox、dispatch、MCP | `tool-runtime` |
|
||||
| 窗口、系统文件选择、通知、Dock、tray、updater、sidecar | Electron Desktop Host |
|
||||
| UI request builder、response normalization、projection | Renderer `src/lib/api/` 或 typed package |
|
||||
| 需求 | Owner |
|
||||
| --------------------------------------------------------------- | --------------------------------------------------- |
|
||||
| Thread / Turn / Item、read model、evidence、业务查询与写入 | App Server protocol + handler + current Rust domain |
|
||||
| 模型路由、canonical content、capability、provider wire lowering | `runtime-core` / `model-provider` |
|
||||
| 工具定义、审批、sandbox、dispatch、MCP | `tool-runtime` |
|
||||
| 窗口、系统文件选择、通知、Dock、tray、updater、sidecar | Electron Desktop Host |
|
||||
| UI request builder、response normalization、projection | Renderer `src/lib/api/` 或 typed package |
|
||||
|
||||
禁止为业务调用新增第二个 Electron 后端、renderer mock fallback、临时 DevBridge 命令或 legacy wrapper。生产失败必须显式失败;mock 仅在测试夹具中显式注入。
|
||||
|
||||
@@ -43,13 +43,13 @@ Renderer typed gateway
|
||||
|
||||
## 验证入口
|
||||
|
||||
| 风险 | 最低验证 |
|
||||
| --- | --- |
|
||||
| Typed client / protocol | `npm run test:contracts` |
|
||||
| Rust domain | `npm run test:rust:related -- <paths...>` |
|
||||
| Desktop bridge | `npm run test:contracts` + `npm run verify:gui-smoke` |
|
||||
| Agent 主链 | `npm run smoke:agent-runtime-current-fixture` |
|
||||
| 真实桌面闭环 | Gate B Electron fixture / GUI smoke |
|
||||
| 风险 | 最低验证 |
|
||||
| ----------------------- | ----------------------------------------------------- |
|
||||
| Typed client / protocol | `npm run test:contracts` |
|
||||
| Rust domain | `npm run test:rust:related -- <paths...>` |
|
||||
| Desktop bridge | `npm run test:contracts` + `npm run verify:gui-smoke` |
|
||||
| Agent 主链 | `npm run smoke:agent-runtime-current-fixture` |
|
||||
| 真实桌面闭环 | Gate B Electron fixture / GUI smoke |
|
||||
|
||||
具体质量选择见 [quality-workflow.md](quality-workflow.md)。
|
||||
|
||||
@@ -59,7 +59,7 @@ MCP 管理、发现和调用只允许走:
|
||||
|
||||
`src/lib/api/mcp.ts -> AppServerClient.request(...) -> app_server_handle_json_lines -> App Server JSON-RPC -> lime-rs/crates/mcp`
|
||||
|
||||
current method 为 `mcpServer/list`、`mcpServerStatus/list`、`mcpServer/create`、`mcpServer/update`、`mcpServer/delete`、`mcpServer/enabled/set`、`mcpServer/importFromApp`、`mcpServer/syncAllToLive`、`mcpServer/oauth/login`、`mcpServer/start`、`mcpServer/stop`、`mcpTool/list`、`mcpTool/listForContext`、`mcpTool/search`、`mcpTool/call`、`mcpTool/callWithCaller`、`mcpPrompt/list`、`mcpPrompt/get`、`mcpResource/list`、`mcpResource/read`、`mcpResource/subscribe` 与 `mcpResource/unsubscribe`。事件 `mcp:resources_updated` 和 `mcp:resource_updated` 必须经真实 MCP manager / Desktop Host event bridge 投影;浏览器模式不得静默退回 mock event fallback。
|
||||
current method 为 `mcpServer/list`、`mcpServerStatus/list`、`mcpServer/create`、`mcpServer/update`、`mcpServer/delete`、`mcpServer/enabled/set`、`mcpServer/importFromApp`、`mcpServer/syncAllToLive`、`mcpServer/oauth/login`、`mcpServer/oauthLogin/completed`、`mcpServer/start`、`mcpServer/stop`、`mcpTool/list`、`mcpTool/listForContext`、`mcpTool/search`、`mcpTool/call`、`mcpTool/callWithCaller`、`mcpPrompt/list`、`mcpPrompt/get`、`mcpResource/list`、`mcpResource/read`、`mcpResource/subscribe` 与 `mcpResource/unsubscribe`。OAuth 完成态只允许由 App Server v2 typed notification `mcpServer/oauthLogin/completed` 投影给 Renderer,不再经过 Desktop event。事件 `mcp:resources_updated` 和 `mcp:resource_updated` 必须经真实 MCP manager / Desktop Host event bridge 投影;浏览器模式不得静默退回 mock event fallback。
|
||||
|
||||
live evidence 仅通过 `smoke:mcp-current -- --allow-live-provider` 显式开启,且需要 `LIME_MCP_LIVE_SERVER_URL`。该 URL 不得包含 username、password、query 或 hash;认证只能引用环境变量名,不允许 inline secret。`network-invoke.json` 仅可记录脱敏的 host、环境变量名、header 名、范围和工具/资源摘要。
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ src/lib/api/
|
||||
|
||||
src/hooks/
|
||||
├── useMcp.ts # MCP runtime 状态与事件刷新
|
||||
└── useMcpEvents.ts # Desktop Host MCP event bridge
|
||||
└── useMcpEvents.ts # MCP Desktop events 与 App Server typed notification bridge
|
||||
|
||||
src/components/mcp/
|
||||
├── McpPage.tsx # 设置页入口
|
||||
@@ -54,6 +54,7 @@ Current MCP method 固定为:
|
||||
- `mcpServer/importFromApp`
|
||||
- `mcpServer/syncAllToLive`
|
||||
- `mcpServer/oauth/login`
|
||||
- `mcpServer/oauthLogin/completed`
|
||||
- `mcpServer/start`
|
||||
- `mcpServer/stop`
|
||||
- `mcpTool/list`
|
||||
@@ -80,7 +81,7 @@ Current MCP method 固定为:
|
||||
|
||||
- `stdio` 与 `streamable_http` 都由 `lime-rs/crates/mcp` 管理。
|
||||
- HTTP header 只允许来自配置中的安全字段或环境变量引用;inline secret、非法 header、缺失 env var 必须 fail closed。
|
||||
- OAuth 登录走 `mcpServer/oauth/login`,使用系统浏览器 current 网关打开授权 URL,callback 完成后通过 `mcp:oauth_completed` 刷新前端。
|
||||
- OAuth 登录走 `mcpServer/oauth/login`,使用系统浏览器 current 网关打开授权 URL;callback 成功或失败后由 App Server 发布 `mcpServer/oauthLogin/completed` typed notification,Renderer 经 typed event bus 刷新 server/tool 状态。旧 `mcp:oauth_completed` Desktop event 为 `dead / deleted / forbidden-to-restore`。
|
||||
- OAuth token store 使用 app data runtime 下的 versioned credential envelope,按 server name + URL 隔离。
|
||||
- 显式 `oauth.client_id` / `oauth_resource` 目前仍按 unsupported fail-closed,不能伪装成可用。
|
||||
|
||||
@@ -119,6 +120,7 @@ MCP smoke:
|
||||
npm run smoke:mcp-current
|
||||
npm run smoke:mcp-current -- --allow-write-fixture
|
||||
npm run smoke:mcp-current -- --allow-oauth-fixture
|
||||
npm run smoke:mcp-oauth-notification-electron-fixture
|
||||
npm run smoke:mcp-current -- --allow-live-provider
|
||||
```
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
- 旧 Tauri wrapper 清理入口已合并到生产命令 current 迁移计划;历史 quick cleanup 队列和机械 inventory 文件已删除,不再作为 current 执行计划入口。
|
||||
- Provider 模型能力 taxonomy 进度日志:`internal/exec-plans/provider-model-taxonomy-progress.md`
|
||||
- Windows Lime Hub 模型路由修复:`internal/exec-plans/windows-runtime-model-route-repair-plan.md`
|
||||
- Windows 模型目录与更新可靠性闭环:`internal/exec-plans/windows-model-catalog-and-updater-reliability.md`
|
||||
- Lime 核心用户主流程 E2E 审计:`internal/exec-plans/core-user-flow-e2e-audit-2026-07-31.md`
|
||||
- MCP 现代化进度:`internal/exec-plans/mcp-modernization-progress.md`
|
||||
- Right Surface 统一承载实施进度:`internal/exec-plans/right-surface-implementation-progress.md`
|
||||
- Soul Style 输出面收敛计划:`internal/exec-plans/soul-style-output-surface-convergence-plan.md`
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# Lime 核心用户主流程 E2E 审计
|
||||
|
||||
状态:本轮范围完成;Windows packaged updater 待实机验收
|
||||
日期:2026-07-31
|
||||
|
||||
## 目标与用户闭环
|
||||
|
||||
普通桌面用户从启动 Lime 开始,完成 Provider / 模型确认,创建对话并完成一轮响应,再验证停止后继续、历史恢复、关键设置与更新状态。完成标准不是页面可打开,而是 GUI 状态、Electron IPC、App Server current JSON-RPC 与 read model identity 一致,生产 mock fallback 命中为零。
|
||||
|
||||
## 范围
|
||||
|
||||
- 启动、首页加载、主导航、控制台基线。
|
||||
- `设置 -> AI 服务商` 的 Provider 列表、显式模型可见性与返回聊天后的模型选择。
|
||||
- Claw 新会话、用户消息、流式 assistant 输出与真实 terminal。
|
||||
- 停止、输入框立即恢复、同一会话继续下一轮。
|
||||
- 最近对话 / 历史详情恢复与 Electron 重启后的 read model。
|
||||
- `设置 -> 关于` 的当前版本与更新状态语义。
|
||||
|
||||
不做范围:不修改真实用户 Provider 凭证;不保存配置、API key、完整 prompt 或 provider 响应;不把 external fixture 当作 live Provider;不宣称 macOS Gate B 能替代 Windows Squirrel packaged 验收;不覆盖社媒内容、素材、浏览器自动化、MCP、Agent Apps 和所有 `@` 命令的产品矩阵。
|
||||
|
||||
## 写集
|
||||
|
||||
- 本执行记录。
|
||||
- `src/components/api-key-provider/ModelAddPanel.tsx`。
|
||||
- `src/components/api-key-provider/ApiKeyProviderSection.ui.test.tsx`。
|
||||
- `src/RootRouter.tsx`、`src/RootRouter.test.tsx`。
|
||||
- `src/components/ui/sonner.test.tsx`。
|
||||
- `src/components/agent/chat/utils/agentRuntimeErrorPresentation.ts` 及其测试。
|
||||
- `src/components/agent/chat/hooks/agentStreamSubmitFailure.ts`。
|
||||
- `src/components/agent/chat/workspace/useWorkspaceSendActions.ts` 及其测试。
|
||||
- `src/i18n/resources/{zh-CN,zh-TW,en-US,ja-JP,ko-KR}/settings.json`。
|
||||
- 发现其它缺陷后,只扩展到最接近根因的 current owner、定向测试与五语言资源,并在修改前登记。
|
||||
|
||||
避让:`internal/aiprompts/architecture.md`、`internal/exec-plans/refactor-v2-implementation.md`、App Server protocol / Skills runtime 并行脏区,以及当前未归属本任务的工作树改动。
|
||||
|
||||
## Agent Verification Contract
|
||||
|
||||
### 基本信息
|
||||
|
||||
```text
|
||||
改动名称:Lime 核心用户主流程 E2E 审计
|
||||
执行计划文件:internal/exec-plans/core-user-flow-e2e-audit-2026-07-31.md
|
||||
负责人:Codex
|
||||
预算标签:budget:normal
|
||||
风险等级:P0
|
||||
影响模块:Electron Desktop Host、App Server current session、Claw GUI、Provider / model GUI、历史恢复、Updater GUI
|
||||
不做范围:live Provider、真实用户配置写入、Windows packaged installer
|
||||
```
|
||||
|
||||
### Current 主链
|
||||
|
||||
```text
|
||||
前端入口:Claw 首页、模型选择器、设置页、最近对话
|
||||
前端网关:src/lib/api/* typed gateway / AppServerClient
|
||||
Electron Desktop Host bridge:preload contextBridge -> electron-ipc -> app_server_handle_json_lines
|
||||
App Server method:agentSession/* current session surface 及对应 read/list;宿主更新能力不进入业务 JSON-RPC
|
||||
RuntimeCore / service owner:RuntimeCore external fixture backend;Updater 归 electron/updateHost.ts
|
||||
read model:Thread / Turn / Item projection 与 current session read model
|
||||
runtime event:message.delta、turn.completed、turn.canceled 及 current terminal projection
|
||||
Evidence Pack 字段:method、transport、status、sessionId、turnId、scenario marker、mock/console error 计数
|
||||
GUI surface:Claw 输入框、消息列表、停止按钮、最近对话、AI 服务商、关于
|
||||
```
|
||||
|
||||
### Happy Path
|
||||
|
||||
```text
|
||||
用户输入 / Agent 输入:隔离 fixture 中发送无敏感信息的场景 marker
|
||||
预期 runtime events:turn started -> message delta -> turn completed;取消场景为 turn canceled
|
||||
预期 tool calls:基础聊天无需工具;不以工具调用作为完成条件
|
||||
预期 approval / sandbox:无审批;fixture 不写用户工作区
|
||||
预期 artifact:无
|
||||
预期 evidence:Electron/preload/IPC/App Server/runtime/read model/GUI identity 一致
|
||||
预期 GUI 状态:消息可见、terminal 后输入框恢复、历史可重新打开、Provider 显式模型可见、更新状态不误报
|
||||
失败时应停在哪一层:按 Renderer、Electron bridge、App Server、runtime/read model、平台 updater 精确分类
|
||||
```
|
||||
|
||||
### Evidence Layers
|
||||
|
||||
| Layer | 本次是否需要 | 证据路径 / 计划路径 | 不需要的原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| deterministic-smoke | 是 | `smoke:agent-runtime-current-fixture` 与 Claw fixture summaries | - |
|
||||
| gui-trace | 是 | 真实 Electron fixture / CDP,记录脱敏 trace 摘要 | - |
|
||||
| runtime-transcript | 是 | fixture summary 中的 terminal 与 read model identity | - |
|
||||
| release-artifact | 否 | - | 本轮不制作或安装发行包 |
|
||||
|
||||
### 必跑命令
|
||||
|
||||
```bash
|
||||
# C0
|
||||
npm run test:contracts
|
||||
|
||||
# C1
|
||||
npm run smoke:agent-runtime-current-fixture
|
||||
npm run smoke:claw-chat-current-fixture
|
||||
npm run smoke:claw-chat-current-fixture -- --scenario cancel
|
||||
npm run smoke:claw-chat-current-fixture -- --scenario cancel-then-continue
|
||||
npm run smoke:agent-session-history-electron-fixture
|
||||
|
||||
# C2
|
||||
npm run verify:gui-smoke
|
||||
LIME_ELECTRON_REMOTE_DEBUGGING_PORT=9223 npm run electron:dev
|
||||
npm run bridge:health -- --timeout-ms 120000
|
||||
```
|
||||
|
||||
未跑:C3 / C4 qcloop、live Provider、Windows packaged updater。
|
||||
原因:用户未要求发送数据到真实 Provider,本机不是 Windows packaged 环境。
|
||||
风险:不能证明具体线上 Provider 的回答质量、网络稳定性或 Windows 安装器闭环。
|
||||
后续触发条件:明确授权 live Provider,或提供 Windows N-1 packaged 测试机。
|
||||
|
||||
### Agent QC 场景映射
|
||||
|
||||
```text
|
||||
P0: 启动与主链可用、消息可见、真实 terminal、取消、取消后继续、历史恢复
|
||||
P1: Provider / 模型可见性、设置导航、更新状态语义
|
||||
P2: 非主流程页面视觉与扩展能力
|
||||
```
|
||||
|
||||
选择依据:本轮验证用户最基本的可用闭环;允许 deterministic sidecar / external fixture,但 official evidence 只声明到对应 Gate,不冒充 live Provider 或 packaged Windows。
|
||||
|
||||
### Supervisor Rubric
|
||||
|
||||
本轮不使用 LLM Supervisor。确定性断言只判断:主流程能否完成、identity 是否一致、mock / console / invoke error 是否为零。
|
||||
|
||||
### 回写规则
|
||||
|
||||
```text
|
||||
失败类型:产品阻塞、体验误导、信息泄露、桥接缺口、测试缺口、环境噪音
|
||||
回写资产:最接近根因的单元 / 组件测试、fixture smoke、Playwright trace 或 GUI flow
|
||||
关闭条件:修复后同一场景复测通过,且不恢复 compat / mock fallback
|
||||
```
|
||||
|
||||
### 完成标准
|
||||
|
||||
```text
|
||||
主线目标是否完成:是;已完成 controlled current 主链与 macOS Electron Gate B,未扩张到 live Provider 或 Windows packaged claim
|
||||
已跑验证:Provider / updater / typed error 定向回归、contracts、App Server protocol check、current runtime 聚合 fixture、typed error success/failure Gate B、GUI smoke、版本一致性
|
||||
未跑验证及原因:live Provider 与 Windows packaged,见上文
|
||||
是否存在 token / Provider / GUI owner 风险:不调用 live Provider;真实 Electron CDP 独占本轮窗口
|
||||
是否可进入 release evidence:否,缺 Windows packaged 与 live Provider claim
|
||||
下一刀:在 Windows N-1 Squirrel 安装包上完成发现、下载、重启安装与最终版本确认
|
||||
```
|
||||
|
||||
### 架构图确认
|
||||
|
||||
```text
|
||||
架构影响:当前为只读 E2E 审计,尚未改变 owner、协议、read model 或交付门禁
|
||||
架构图已更新:不适用;若修复触达架构敏感路径,将重新分类
|
||||
责任开发者确认:不适用;当前无重大架构变更
|
||||
```
|
||||
|
||||
## 验收矩阵
|
||||
|
||||
| 场景 | Proof level | 状态 | 证据 / 问题 |
|
||||
| --- | --- | --- | --- |
|
||||
| 启动、首页与导航 | Gate B | 通过 | `verify:gui-smoke` 21/21;首页长短输入 current Electron fixture 通过 |
|
||||
| Provider 与显式模型 | Gate A + Gate B bridge | 通过,Windows 平台待补 | Provider / ModelSelector 68/68;Provider hooks / storage / context / integration 26/26;不声明 live Provider |
|
||||
| 新会话、stream、terminal | Current fixture + Gate B | 通过 | 聚合 current fixture 通过;typed error success/failure 均为 Gate B controlled fixture |
|
||||
| 停止后继续 | Current fixture + Gate B | 通过 | cancel、cancel-then-continue 与 active steer 聚合场景通过 |
|
||||
| 历史恢复与重启 | Gate B fixture | 通过 | history/cache hydration、Plan history hydrate 与 Coding Workbench 恢复通过 |
|
||||
| 关于与更新状态 | Gate A + host unit | 通过,Windows packaged 待补 | updater / About / UpdateNotification / Sidebar 51/51;版本 `1.117.0`;Squirrel N-1 实机未验收 |
|
||||
|
||||
## 问题台账
|
||||
|
||||
| 严重度 | 分类 | 场景 | 状态 | 根因 / 修复 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P0 | 桥接缺口 / 构建阻塞 | 任何需要 source 重建 App Server sidecar 的 Electron 主流程 | 已解决 | typed `error` notification 的 Rust 关联项歧义已消除;`cargo check -p app-server-protocol`、host 重建与 GUI smoke 通过 |
|
||||
| P0 | 桥接缺口 / contract 漂移 | Electron host fixture 重建 | 已解决 | generated TypeScript notification union、client 与 fixture 已同步;`test:contracts` 和聚合 Electron fixture 通过 |
|
||||
| P0 | 产品阻塞 / 体验误导 | 自定义 Provider 添加 | 已修复 | 未保存态改为“取消”;保存成功但连接测试失败时,“完成添加”仍激活已持久化 Provider |
|
||||
| P0 | 产品阻塞 | 发送失败反馈 | 已修复并回归 | 恢复全局 Toaster;typed error success/failure Electron fixture 均无 console / invoke error |
|
||||
| P0 | 信息泄露 / 体验误导 | 不可执行模型路由发送 | 已修复并回归 | 用户只看到五语言 Provider 不可用提示,内部路由错误保留在诊断边界 |
|
||||
| P0 | 测试缺口 | typed retry GUI 等待 | 已修复 | fixture 原先只在用户消息分组内查 assistant 状态且错误要求输入框禁用;改用全局 `data-status` 语义状态并验证 active steer 输入可用 |
|
||||
| P0 | 测试缺口 | typed retry trace 聚合 | 已修复 | external backend 在 retry 后漏发 `provider.first_text_delta.received`;补齐 Provider checkpoint,不放宽 Provider / App Server 分离断言 |
|
||||
| P0 | 测试缺口 | typed retry read model | 已修复 | 删除过时的 `detail.thread_read` 私有状态读取,统一使用 canonical `turns[]` owner;completed 等待同时要求真实 terminal |
|
||||
| P0 | 测试缺口 | typed retry failure 中间态 | 已修复 | 不再等待不会进入 `thread/read` 的内部错误 detail;改为证明 prompt 保留且 read model 在 `turn.failed` 前仍为非终态 |
|
||||
| P1 | 可维护性 | Electron fixture 编排文件超过 1000 行 | 已登记退出条件 | `backend-script.mjs`、`smoke.mjs` 与 `smoke.test.mjs` 不再继续承接下一种 backend 场景;新增场景前先把 typed error backend 生成片段和 assertion guard 拆到独立领域模块,聚合入口只保留编排 |
|
||||
|
||||
## 执行日志
|
||||
|
||||
- 2026-07-31:建立 `budget:normal / P0` 审计合同;本机 `1420 / 3030 / 9223` 均无监听,从干净环境开始。
|
||||
- 2026-07-31:`npm run smoke:agent-runtime-current-fixture` 通过;覆盖首页首发、stream terminal、停止后继续、历史 hydrate、审批、计划、Skills / MCP、媒体与工作台,明确 `liveProviderUsed=false`。
|
||||
- 2026-07-31:随后单独运行 `npm run smoke:claw-chat-current-fixture` 时,检测到 13:14 更新的并行 App Server protocol source 比 13:03 的 sidecar 新,重建在 `envelopes.rs:753` 失败;错误为 `ambiguous associated item`。旧聚合证据有效但不能替代当前 source build,新的 Gate B 暂停。
|
||||
- 2026-07-31:测试进程临时设置 `RUSTFLAGS=-Aambiguous-associated-items` 后,`cargo check -p app-server-protocol` 通过,但继续重建 host 时 `packages/app-server-client` 因 `error` notification 未进入生成 method union 而失败。临时 lint 参数只用于继续诊断,不关闭默认构建阻塞。
|
||||
- 2026-07-31:隔离 Electron CDP 复现 Provider 静默丢失:填写 `E2E Local Provider / e2e-explicit-model` 后点击“完成添加”,页面回到“还没有启用模型”,console / invoke error 均为 0;源码确认该按钮直接绑定 `onCancel`。
|
||||
- 2026-07-31:继续复现保存失败分支:本机不可达 endpoint 的配置成功持久化,设置列表显示 `e2e-explicit-model` 且标为默认,但返回首页仍只显示 `gpt-5.2-pro`;根因为完成动作未调用 `onActivated`,导致 persisted provider 与 active provider 分裂。
|
||||
- 2026-07-31:沿不可执行 Provider 真实发送:`agentSession/turn/start` 经 `electron-ipc -> app_server_handle_json_lines` 返回 `runtime model route is not executable`;输入恢复,但无可见 toast。源码确认生产唯一 Toaster 挂载在发布改动中被删除。
|
||||
- 2026-07-31:恢复 `RootRouter` 的 Toaster 挂载,覆盖主应用、Browser Runtime、Resource Manager 和开发 smoke 窗口;新增真实 Sonner DOM 回归。发送提交与流式提交失败统一使用 `resolveAgentRuntimeSubmitErrorMessage`,将不可执行路由隐藏为 Provider 暂不可用五语言文案。
|
||||
- 2026-07-31:定向 Vitest 通过:`RootRouter.test.tsx` 4/4、`sonner.test.tsx` 1/1、`agentRuntimeErrorPresentation.test.ts` 14/14、`useWorkspaceSendActions.test.tsx` 155/155。
|
||||
- 2026-07-31:修复 typed error fixture 的 DOM 作用域、`data-status`、Provider checkpoint、canonical read model 状态读取与 pre-terminal 等待合同;fixture guard 81/81 通过。
|
||||
- 2026-07-31:typed error / runtime status 最终定向回归 93/93 通过;全工作树 `git diff --check` 通过,fixture 进程无残留。
|
||||
- 2026-07-31:`typed-error-retry-success` 与 `typed-error-retry-failure` 真实 Electron controlled Gate B 均通过;两者均命中 Electron IPC、App Server current JSON-RPC、Provider/App Server trace 分离、同一 turn identity,console / invoke error 为零。
|
||||
- 2026-07-31:`npm run smoke:agent-runtime-current-fixture` 完整通过;覆盖首页、历史恢复、停止继续、审批、active steer、Plan、Skills、MCP、媒体、代码工作台、typed error success/failure 与内容工厂,`liveProviderUsed=false`。
|
||||
- 2026-07-31:`npm run verify:gui-smoke` 通过,21/21 assertions;evidence:`.lime/qc/project-gates/standalone-shell-01-20260731103342-78558/shell-01-electron-smoke/summary.json`。
|
||||
- 2026-07-31:本轮 controlled 主流程完成。Windows packaged Squirrel 从 N-1 到新版本的发现、下载、重启安装与最终版本确认仍是唯一发布平台阻塞,完成度 92%。
|
||||
@@ -9,6 +9,7 @@
|
||||
## 分类
|
||||
|
||||
- `current`:`lime-rs/crates/mcp`、App Server `mcp*` JSON-RPC methods、前端 `src/lib/api/mcp.ts`
|
||||
- `current`:OAuth completion 只由 App Server `mcpServer/oauthLogin/completed` typed notification 投影给 Renderer
|
||||
- `current`:runtime 工具命名 `mcp__<server>__<tool>`,extension surface key 继续使用 `mcp__<server>`
|
||||
- `current`:`ToolSearchBridgeTool` 消费 MCP inventory / search 结果
|
||||
- `test-only`:retired MCP facade 只允许停留在 contract forbidden snippet、smoke 观测黑名单与负向测试文本中
|
||||
@@ -47,11 +48,11 @@
|
||||
- App Server protocol schema、schema fixtures、npm client generated types、package client 测试与 MCP API fail-closed 测试已同步 `mcpServer/oauth/login`。
|
||||
- MCP 管理面已消费 `runtime_status.auth_status.action_plan`:动态 OAuth 缺授权时显示登录入口,调用 `mcpServer/oauth/login` 并用系统浏览器打开 `authorizationUrl`;显式 `client_id/oauth_resource` unsupported 状态只显示不可用提示,不提供假登录入口;已授权状态显示为“已授权”。
|
||||
- MCP OAuth 登录入口已补五语言 `settings` 文案与 `McpPanel` 组件回归,覆盖登录打开、unsupported fail-closed 和已授权状态。
|
||||
- OAuth callback 完成后会发出 `mcp:oauth_completed` 事件,前端 `useMcp` 监听事件后自动刷新 server/tool 列表,`McpPanel` 给出“授权已完成,状态已刷新”的五语言 toast。
|
||||
- 参考 Codex 的 `mcpServer/oauth/login -> async completion notification` 设计后,补齐本地回环 OAuth provider completion 回归:测试真实跑 rmcp metadata discovery、dynamic client registration、authorization redirect、callback、token exchange、持久 token store 与 `mcp:oauth_completed` 事件发射。
|
||||
- OAuth callback 完成后由 App Server 发出 `mcpServer/oauthLogin/completed` typed notification,前端 `useMcp` 经 typed event bus 自动刷新 server/tool 列表,`McpPanel` 给出“授权已完成,状态已刷新”的五语言 toast。
|
||||
- 参考 Codex 的 `mcpServer/oauth/login -> async completion notification` 设计后,补齐本地回环 OAuth provider completion 回归:测试真实跑 rmcp metadata discovery、dynamic client registration、authorization redirect、callback、token exchange、持久 token store 与 App Server typed notification 投影。
|
||||
- 动态 OAuth 登录现在复用 streamable HTTP 的 `http_headers` / `env_http_headers` 构造路径;metadata discovery、dynamic client registration、持久凭据恢复和授权后的 MCP HTTP transport 使用同一组默认 headers,避免 OAuth 登录链路与普通 streamable HTTP 链路分叉。
|
||||
- 动态 OAuth scopes 对齐 Codex 解析优先级:登录参数 scopes > server 配置 scopes > provider discovery `scopes_supported` > 空;discovery scopes 会去重、trim 并过滤空值,只在用户未显式传入且 server 未配置 scopes 时启用。
|
||||
- OAuth callback 支持标准 provider error 回调(如 `error=invalid_scope&error_description=...`),不再等到 5 分钟超时;后台立即通过既有 `mcp:server_error` 事件把失败投影给 GUI。
|
||||
- OAuth callback 支持标准 provider error 回调(如 `error=invalid_scope&error_description=...`),不再等到 5 分钟超时;App Server 立即通过 `mcpServer/oauthLogin/completed { success: false, error }` 把失败投影给 GUI。
|
||||
- OAuth 回归测试已拆到 `lime-rs/crates/mcp/src/oauth_tests.rs`,`oauth.rs` 保持在代码体量边界内;`manager.rs` 巨型文件未继续追加新逻辑。
|
||||
- MCP manager 已按职责拆分为 `manager/lifecycle.rs`、`manager/tools.rs`、`manager/prompts.rs`、`manager/resources.rs` 与 `manager/tests/*`;`manager.rs` 从 3000+ 行收敛到状态、连接池、缓存与事件 facade,当前最大子模块约 533 行,避免后续 transport / 工具 / prompt / resource 逻辑继续堆回中心文件。
|
||||
- MCP GUI OAuth 登录打开授权页已从 `window.open` 收敛到 `openExternalUrlWithSystemBrowser` current 外链网关,避免 Electron 下绕过 Desktop Host 的浏览器打开链路;组件回归覆盖授权 URL 通过系统浏览器网关打开且不回退 `window.open`。
|
||||
@@ -310,10 +311,10 @@
|
||||
- `git diff --check -- "scripts/mcp/lib/current-smoke-core.mjs" "scripts/mcp/lib/current-smoke-transport.mjs" "scripts/mcp/current-smoke.test.mjs" "scripts/mcp/lib/contract-guards.mjs" "internal/exec-plans/mcp-modernization-progress.md"`
|
||||
- `rg -n "[ \t]+$" -- "scripts/mcp/lib/current-smoke-core.mjs" "scripts/mcp/lib/current-smoke-transport.mjs" "scripts/mcp/current-smoke.test.mjs" "scripts/mcp/lib/contract-guards.mjs" "internal/exec-plans/mcp-modernization-progress.md"`:无命中。
|
||||
- `rg -n "^(<<<<<<<|=======|>>>>>>>)" -- "scripts/mcp/lib/current-smoke-core.mjs" "scripts/mcp/lib/current-smoke-transport.mjs" "scripts/mcp/current-smoke.test.mjs" "scripts/mcp/lib/contract-guards.mjs" "internal/exec-plans/mcp-modernization-progress.md"`:无命中。
|
||||
- MCP Desktop Host event bridge 监听已从 `src/hooks/useMcp.ts` 拆到 `src/hooks/useMcpEvents.ts`:事件 payload 类型、`safeListen` 注册、`mcp:server_started/stopped/error/tools_updated/resources_updated/resource_updated/oauth_completed` 处理集中在事件模块;`useMcp.ts` 只保留状态、action 与初始化编排,文件体量从约 553 行降到约 420 行。
|
||||
- `scripts/mcp/lib/contract-guards.mjs` 已纳入 `src/hooks/useMcpEvents.ts`,强制 MCP runtime events 继续通过 `safeListen` current bridge 注册,并禁止 `defaultMocks`、`mockPriorityCommands`、`invokeMockOnly`、`explicitMockFallback`、`listenExplicitMock` 回流到事件监听边界。
|
||||
- MCP event 监听已从 `src/hooks/useMcp.ts` 拆到 `src/hooks/useMcpEvents.ts`:`mcp:server_started/stopped/error/tools_updated/resources_updated/resource_updated` 继续通过 Desktop `safeListen`,OAuth completion 改为同步订阅 App Server typed event bus;`useMcp.ts` 只保留状态、action 与初始化编排。
|
||||
- `scripts/mcp/lib/contract-guards.mjs` 已纳入 `src/hooks/useMcpEvents.ts`,强制剩余 MCP Desktop runtime events 继续通过 `safeListen` current bridge、OAuth completion 只走 typed notification,并禁止 legacy OAuth event 与生产 mock fallback 回流。
|
||||
- `npx eslint "src/hooks/useMcp.ts" "src/hooks/useMcpEvents.ts" "src/hooks/useMcp.test.tsx" --max-warnings 0`
|
||||
- `npx vitest run "src/hooks/useMcp.test.tsx" --silent=passed-only --disableConsoleIntercept --testTimeout=30000`:6 passed,覆盖 OAuth completion、resources updated、resource updated 与订阅 current API 透传。
|
||||
- `npx vitest run "src/hooks/useMcp.test.tsx" --silent=passed-only --disableConsoleIntercept --testTimeout=30000`:9 passed,覆盖 typed OAuth success/failure、Desktop listener 阻塞时同步订阅、resources updated、resource updated 与订阅 current API 透传。
|
||||
- `node --check "scripts/mcp/lib/contract-guards.mjs" && node "scripts/check-app-server-client-contract.mjs"`:通过,281 checks,MCP event bridge 守卫已纳入 App Server client contract guard。
|
||||
- `npm run test:contracts`:通过,含 protocol types、App Server client contract、command contracts、mock priority commands=0、script governance、electron release workflow guard、harness cleanup 与 docs boundary。
|
||||
- MCP 前端 API response guard 已从 `src/lib/api/mcp.ts` 拆到 `src/lib/api/mcpResponseGuards.ts`:`mcp.ts` 保留 `AppServerClient.request(...)`、App Server method 常量和 `mcpApi` 编排;response guard 模块集中校验 server list、lifecycle empty result、OAuth login、tool result、prompt result、resource content 与 resource templates,文件体量从约 512 行降到约 367 行。
|
||||
@@ -410,13 +411,16 @@
|
||||
- 2026-06-22:清理 `smoke:agent-runtime-tool-execution -- --batch runtime-introspection-tools` 的旧 ToolSearch 参数,移除 `caller / limit / include_deferred / include_schema`,只保留 current schema `query / max_results`;复跑该 smoke 通过,证明 ToolSearch 与 SendUserMessage 均完成。
|
||||
- 已验证:`cargo fmt --all`;`cargo test --manifest-path "lime-rs/Cargo.toml" -p app-server runtime_backend_tool_inventory`(3 passed);`npm run smoke:agent-runtime-tool-execution -- --batch runtime-introspection-tools --output /tmp/lime-runtime-introspection-smoke.json --timeout-ms 180000`;`cargo test --manifest-path "lime-rs/Cargo.toml" -p lime-mcp streamable_http --lib`(13 passed);`npx vitest run "src/components/mcp/McpPage.test.tsx" --silent=passed-only --disableConsoleIntercept --testTimeout=30000`(4 passed);五语言 `settings.json` JSON parse;`npx eslint "src/components/mcp/mcpPageModel.ts" "src/components/mcp/McpPageEditor.tsx" "src/components/mcp/McpPage.test.tsx" --max-warnings 0`;`npm run smoke:mcp-current -- --allow-write-fixture --timeout-ms 120000`;Context7 `--allow-live-provider` + `env_http_headers` live smoke;`npx vitest run "scripts/mcp/current-smoke.test.mjs" --silent=passed-only --disableConsoleIntercept --testTimeout=30000`(6 passed);`npm run test:contracts`;`npm run verify:gui-smoke`;`git diff --check -- "lime-rs/crates/app-server/src/runtime_backend/tool_inventory.rs" "lime-rs/crates/app-server/src/runtime_backend/tests/tool_inventory.rs" "lime-rs/crates/mcp/src/streamable_http.rs" "scripts/agent-runtime/tool-execution-smoke.mjs" "src/components/mcp/mcpPageModel.ts" "src/components/mcp/McpPageEditor.tsx" "src/components/mcp/McpPage.test.tsx" "src/i18n/resources/zh-CN/settings.json" "src/i18n/resources/zh-TW/settings.json" "src/i18n/resources/en-US/settings.json" "src/i18n/resources/ja-JP/settings.json" "src/i18n/resources/ko-KR/settings.json"`。
|
||||
|
||||
- 2026-07-31:OAuth completion 已从 `mcp:oauth_completed` Desktop event 迁到 App Server `mcpServer/oauthLogin/completed` typed notification。Rust MCP 只返回 completion handle;App Server 投影成功/失败;Renderer 在任何异步 Desktop listener 前同步订阅 typed event bus,并自动刷新 status/tools。旧事件字符串受 contract guard 禁止回流 `electron`、`lime-rs`、`packages` 与 `src`。
|
||||
- 2026-07-31:`npm run smoke:mcp-oauth-notification-electron-fixture` 专用 Gate B 通过。证据 `.lime/qc/mcp-oauth-notification/mcp-oauth-notification-fixture-summary.json` 为 `ok=true`:真实 Electron/preload/IPC、`app_server_handle_json_lines`、App Server event drain、本地 OAuth provider callback 与 GUI 状态/toast 全链命中;自动刷新 `mcpServer/oauth/login`、`mcpServerStatus/list`、`mcpTool/list`,`mockFallbackHitCount=0`、`failedInvokeCount=0`、console/page/invoke errors 为 0。
|
||||
|
||||
## 当前缺口
|
||||
|
||||
- OAuth、动态授权与用户 approval 已具备 GUI 登录入口、completion 自动刷新闭环、本地回环 OAuth provider completion 证据,以及 `smoke:mcp-current -- --allow-oauth-fixture` 的 Electron `open_external_url` 网关证据;下一刀应补真实第三方 provider 账号流的 live-gated / 手工 E2E 证据。
|
||||
- OAuth、动态授权与用户 approval 已具备 GUI 登录入口、typed completion 自动刷新闭环、本地回环 OAuth provider completion 证据、`smoke:mcp-current -- --allow-oauth-fixture` 的 Electron `open_external_url` 网关证据,以及 `smoke:mcp-oauth-notification-electron-fixture` 的专用真实 Electron Gate B;下一刀应补真实第三方 provider 账号流的 live-gated / 手工 E2E 证据。
|
||||
- 显式 `oauth.client_id` / `oauth_resource` 仍按 unsupported fail-closed 处理,后续需要等 connector 能无损支持后再打开。
|
||||
- rmcp 0.12 的 `exchange_code_for_token` 内部仍新建 reqwest client;Lime 目前能保证 OAuth discovery / dynamic registration / authorized MCP requests 复用自定义 headers,但不能无损保证 token endpoint 也接收自定义 headers。后续若要支持需要 upstream connector seam 或本地 OAuth flow 封装,不应靠假支持打开。
|
||||
- rmcp 0.12 的 token exchange 内部自建 client 也意味着本地 loopback OAuth provider 在带系统代理的开发机上需要测试进程级 `NO_PROXY` 保护;当前保护只放在 Rust fixture 测试里,不修改生产进程环境。
|
||||
- MCP GUI 管理面仍缺真实第三方 OAuth provider 账号流 smoke;当前本地 OAuth fixture 已证明系统浏览器 current 网关、provider redirect、callback completion 与状态回流闭环。
|
||||
- MCP GUI 管理面仍缺真实第三方 OAuth provider 账号流 smoke;当前本地 OAuth fixture 已证明系统浏览器 current 网关、provider redirect、callback completion、App Server typed notification、自动 status/tool refresh 与 GUI 状态/toast 回流闭环。
|
||||
- MCP `structuredContent` control-plane、runtime/read model/evidence projection、基础 Agent Chat 展示回归与真实 Electron fixture 可见证据已覆盖;下一刀若继续提升完整度,应补 Playwright 人工交互式证据或真实第三方 MCP provider 工具结果的 live-gated E2E。
|
||||
- `smoke:mcp-current -- --allow-live-provider` 已对 Context7 远程 provider 跑通 `resolve-library-id` 与 `query-docs` 两条真实 tool E2E,且 `isError=true` 已 fail-closed;`smoke:mcp-config-electron-fixture` 已补真实 Electron 设置页创建 Context7 配置并经 `app_server_handle_json_lines -> mcpServer/list` 验证落库的 GUI 证据;`smoke:mcp-context7-live-electron-fixture` 已补真实 Electron 启动 / ToolSearch / tool call 远程 Context7 live 入口。剩余缺口是在用户本机显式设置真实 `CONTEXT7_API_KEY` 后复跑同一 live fixture,确认 header 注入语义;当前本机未设置时仍可验证 Context7 公共工具链路。
|
||||
- Agent Runtime / Claw fixture 拆分后,current 聚合回归、WebTools rendering、Skills Runtime、Expert Skills Runtime 与 `cancel-then-continue` 已恢复到绿色;下一刀应继续补用户截图中更细的 Codex 桌面 UI 视觉一致性断言,例如未完成时不得折叠、搜索 / 思考 / final Markdown 的分段快照和完成前状态稳定性,而不是继续扩张单个 fixture 文件。
|
||||
@@ -442,7 +446,7 @@
|
||||
|
||||
- `npm run verify:local` 本轮尝试已通过 `verify:app-version`、i18n resource check、i18n unused、全仓 eslint 与变更文件 i18n hardcoded scan;进入全量 `tsc --noEmit` 后因同一工作区已有多个并行 `verify:local` / `tsc --noEmit` 高 CPU 进程抢资源,长时间无输出,已主动终止本轮额外验证会话(退出码 130)。本轮 MCP 定向校验、contract 与 GUI smoke 已通过。
|
||||
- 历史上 `npm run test:contracts` 曾被非 MCP `useAgentChat.ts` 脏改动阻塞;当前复跑已通过,MCP current smoke structuredContent 守卫已进入同一 contract 入口。
|
||||
- 本轮尝试 `npm run smoke:mcp-current -- --allow-oauth-fixture --timeout-ms 30000`,当前环境没有运行 `npm run electron:dev` / DevBridge,health check 直接 `fetch failed`;该 smoke 未能作为 GUI 证据使用,需在 Electron DevBridge 运行后复跑。
|
||||
- 历史尝试 `npm run smoke:mcp-current -- --allow-oauth-fixture --timeout-ms 30000` 时因未运行 `npm run electron:dev` / DevBridge 而 `fetch failed`,当时不能作为 GUI 证据;该阻塞现已由不依赖 DevBridge 的 `npm run smoke:mcp-oauth-notification-electron-fixture` 专用真实 Electron Gate B 关闭,旧失败只保留为历史记录。
|
||||
- `scripts/mcp/current-smoke.mjs` 超 1000 行阻塞已收口,当前入口约 390 行,core 约 409 行,transport 约 340 行,fixture writer 约 157 行;后续继续扩 `smoke:mcp-current` 必须优先复用 `scripts/mcp/lib/current-smoke-core.mjs` / `current-smoke-transport.mjs` / `current-smoke-fixture.mjs` / `live-provider-smoke.mjs` 或继续按职责拆子模块,不得把 transport / fixture / assertion helper 长回入口。MCP runtime current contract 已从 `scripts/check-app-server-client-contract.mjs` 拆到 `scripts/mcp/lib/contract-guards.mjs`;根 contract 脚本当前约 8447 行,仍超过非生成代码体量边界,后续应继续按领域把非 MCP contract guard 拆出,不要把新领域规则继续追加到根脚本。
|
||||
- Agent stream handler 超 1000 行阻塞已在 2026-06-22 收口:`agentStreamRuntimeHandler.ts` 当前约 986 行,后续不得再把 runtime lifecycle / terminal / timer 逻辑追加回中心 handler。剩余巨型 facade 是 `src/components/agent/chat/AgentChatWorkspace.tsx`,当前约 5589 行;下一刀如果继续治理 Agent Chat 工作区,应先按 session 列表 / task file runtime / right surface orchestration 分块拆分,并保持 `smoke:agent-runtime-current-fixture`、WebTools rendering fixture 与 `verify:gui-smoke` 绿色。
|
||||
- 本轮曾并行复用 `/tmp/lime-target-mcp-resource-templates` 触发 cargo incremental / rmeta 写入竞争(`No such file or directory (os error 2)`);已改用独立 `CARGO_TARGET_DIR="/tmp/lime-target-appserver-mcp-resource-templates"` 复跑 App Server MCP fixture 并通过。
|
||||
|
||||
@@ -1987,7 +1987,7 @@ OPEN_REF:继续按 EVENT-PROJECTIONS 推进其余 planned notification、Windo
|
||||
|
||||
验证:protocol v2 50/50、schema fixture 1/1、App Server warning 12/12、projection-summary recovery 1/1、Renderer 3 files / 61 tests、generated TypeScript drift check 通过。npm run test:contracts 通过,含 839 schema definitions、831 generated types、0 generation failure、289 app-server-client checks 及 command/harness/modality/scripts/release/docs guards。Rust related 覆盖 19 个 scoped packages、18 个 lib 目标,共 3597 项(3593 passed、4 个既有环境/live 测试 ignored、0 failed)。
|
||||
|
||||
产品证据:npm run smoke:agent-runtime-current-fixture 完整通过且 liveProviderUsed=false;它证明 current Agent/Electron fixture 主链无回归,但没有普通 warning 专项 DOM 断言,因此不冒充 warning 专项 Gate B。通用 npm run verify:gui-smoke 通过 21/21 assertions,证据为 .lime/qc/project-gates/standalone-shell-01-20260730191439-55464/shell-01-electron-smoke/summary.json;该 Gate B-F 证明 Electron/preload/IPC/app_server_handle_json_lines/App Server/GUI shell 与零 mock fallback,不宣称实际命中 warning method。
|
||||
产品证据:npm run smoke:agent-runtime-current-fixture 完整通过且 liveProviderUsed=false;它证明 current Agent/Electron fixture 主链无回归,但没有普通 warning 专项 DOM 断言,因此不冒充 warning 专项 Gate B。fresh npm run verify:local 通过 i18n、lint、typecheck、112/112 Vitest、contracts、Rust changed-scope、Electron renderer/host build 与真实 GUI smoke;后者通过 21/21 assertions,证据为 .lime/qc/project-gates/standalone-shell-01-20260730194114-24572/shell-01-electron-smoke/summary.json。该 Gate B-F 证明 Electron/preload/IPC/app_server_handle_json_lines/App Server/GUI shell 与零 mock fallback,不宣称实际命中 warning method。
|
||||
|
||||
治理与格式:cargo fmt --manifest-path lime-rs/Cargo.toml --all --check、定向 Prettier、git diff --check 与 npm run governance:legacy-report 通过;治理扫描为零引用候选 0、分类漂移 0、边界违规 0。
|
||||
|
||||
@@ -1998,6 +1998,108 @@ OPEN_REF:继续按 EVENT-PROJECTIONS 推进其余 planned notification、Windo
|
||||
OPEN_REF:下一刀审计 skills/changed 的真实 Skill catalog producer、GUI refresh consumer 与 recovery 语义;只有形成 typed producer -> GUI refresh -> 风险匹配 Gate B 的垂直链才转 current。v2 总体完成度保守估算约 91%,仍非 release-ready。
|
||||
```
|
||||
|
||||
### 9.72 V2-05 skills/changed catalog invalidation closure
|
||||
|
||||
```text
|
||||
状态:completed(2026-07-31);只关闭 `skills/changed` catalog invalidation 垂直切片,V2-05 与 v2 总体仍为 in-progress,不是 release-ready。
|
||||
|
||||
主目标:按 Codex `skills/changed` 的严格空对象、进程级瞬时失效语义,在唯一产品链内形成默认 Skill root / 成功本地 mutation -> App Server typed notification -> Renderer 重新读取 current Skill catalog -> GUI 可见更新;不进入 Thread/Turn/Item、持久化或恢复重放,不新增 `skills/extraRoots/set`、第二 catalog owner 或生产 mock fallback。
|
||||
|
||||
本轮窄写集:app-server-protocol v2 notification/method/schema/generated client;lime-skills snapshot cache invalidation;App Server Skill mutation producer/default-root watcher/public JSON-RPC tests;app-server-client typed decoder;Composer Skill hook、current catalog projection 与 focused tests;Skills runtime Gate B fixture;EVENT-PROJECTIONS、architecture、refactor v2 实施计划与本执行计划。共享工作树其它 provider、warning、guardian、realtime、Windows host capability 与 recovery 热区未做无关改写。
|
||||
|
||||
退出条件:`skills/changed` 只接受 `{}` 且进入 protocol catalog/schema/generated client;默认 root 在启动时存在、启动后首次创建、修改和删除时均可失效并按 10 秒节流;成功 Lime catalog mutation 发通知,失败及其他 app mutation 不发;Renderer 只消费 typed event bus 并重新调用 current `skill/list`,重连仍主动 list;Rust/TS/contracts/governance 通过;风险匹配 Gate B 证明真实 Electron/preload/IPC/App Server notification drain、重新 list、GUI catalog 更新与零 mock fallback。
|
||||
|
||||
实现事实:v2 protocol、schema、generated client 与 typed decoder 只接受严格空对象 `skills/changed {}`。App Server 启动默认 Skill root watcher;已存在 root 的创建/修改/删除和启动后首次出现均失效 `lime-skills` snapshot cache 并按 10 秒节流广播 typed notification。成功的 Lime catalog mutation 在 processor 边界失效同一 cache 并附带 notification;失败 mutation 和其他 app mutation 不发。没有新增 `skills/extraRoots/set`。
|
||||
|
||||
Renderer 事实:Composer `useLimeSkills` 的初始加载与 notification refresh 统一调用 current `skill/list`,`skillsApi.getRuntimeCatalog()` 复用 `skillExecutionApi.listExecutableSkills()` 的既有 decoder,再经纯 projection 映射 GUI shape。管理中心 `skillManagement/list` 保持原语义,不成为 Composer catalog owner。Renderer 只消费 typed event bus;命中后写 console-only `skillsChanged.received { method, count }` marker 并自动重读;重连/重新挂载仍主动 list。该事件是进程级瞬时失效,不进入 Thread/Turn/Item、持久化或恢复重放。
|
||||
|
||||
验证:纯 projection、API、Hook 与 fixture guard 共 4 files / 99 tests 通过;npm run typecheck、npm run test:contracts(840 schema definitions、832 generated types、0 generation failures、290 app-server-client checks)、npm run governance:legacy-report、Prettier、MJS node --check、cargo fmt --all --check 与 git diff --check 通过。npm run test:related 因仓库既有 runner 将 electron/ 目录当文件读取而报 EISDIR,未记为通过;本轮精确 Vitest 已覆盖相关前端回归。Rust related 因 Cargo.lock 自动扩大到 workspace lib;首次运行只暴露 protocol::v2::tests::typed_v2_envelope_schema_names_are_stable 的稳定清单漏项,补入 skills/changed 后 app-server-protocol 95/95 通过;workspace lib 最终汇总为 32 个 suite、4670 passed、5 ignored、0 failed。
|
||||
|
||||
产品证据:npm run smoke:claw-chat-current-fixture -- --scenario skills-runtime 通过,proof level 为 Gate B controlled fixture。真实 Electron/preload/IPC/app_server_handle_json_lines/App Server 链先完成初始 skill/list;隔离 HOME 默认 root 新建 notification-refresh/SKILL.md 后,typed skills/changed marker 为 1,skill/list 自动由 1 次增至 2 次且 transport=electron-ipc,GUI 新 Skill 可见,手动刷新点击为 0;console/page/actionable errors 与 failed assertions 均为 0。证据位于 .lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-summary.json。
|
||||
|
||||
整合复验:npm run smoke:agent-runtime-current-fixture 完整通过且 liveProviderUsed=false,包含 skills/changed、Inputbar rich restore、Skills Runtime 三入口及其余 current Agent/Electron fixture。首次聚合运行暴露的 Inputbar 失败属于 fixture DOM 定位合同:夹具用展示名 Capability Report 大小写敏感匹配 current catalog 的 capability-report,候选已可见但 target.click 从未执行;生产 CharacterMention 与 catalog identity 无缺陷。夹具现统一折叠大小写、空格、连字符和下划线,并由 source guard 锁定候选与 @capability-report badge 的同一等价规则。独立 inputbar-rich-restore Gate B 随后通过,text/image/path/skill 在 output-free cancel 后全部恢复,console/page error 与 mock fallback 为 0;证据为 .lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-inputbar-rich-restore-regression-summary.json。
|
||||
|
||||
通用 npm run verify:gui-smoke 通过,真实 Electron renderer、App Server sidecar、Claw shell reload 与 memory settings 均就绪;证据为 .lime/qc/project-gates/standalone-shell-01-20260731043417-63564/shell-01-electron-smoke/summary.json。fresh npm run verify:local 通过版本一致性和 i18n unused 后,在全仓 lint 被非本切片脏文件 src/components/input-kit/ModelSelector.tsx:432 的既有 react-hooks/exhaustive-deps warning 阻断,故未记为通过;本切片未改该文件。后续独立 npm run typecheck、contracts、99 项精确测试、GUI smoke、Rust fmt 与 Gate B 均明确通过。
|
||||
|
||||
架构确认:本切片把 Skill catalog mutation/filesystem invalidation、App Server typed notification 与 Renderer current list refresh 收敛到单一产品链,已同步 internal/aiprompts/architecture.md 第 28 节。责任开发者确认:root,2026-07-31。
|
||||
|
||||
治理分类:v2 skills/changed protocol/schema、App Server watcher/mutation producer、lime-skills cache invalidation、typed Renderer event bus 与 current skill/list GUI refresh 为 current;无 compat;skillManagement/list 仅保留管理中心读取语义;持久化/replay、skills/extraRoots/set、第二 catalog owner 与生产 mock fallback 均未新增。
|
||||
|
||||
OPEN_REF:下一刀回到 EVENT-PROJECTIONS.md,审计下一项具备真实 producer/consumer 的 planned notification、host capability 或 recovery;guardianWarning 在 Guardian runtime producer 落地前保持 planned。v2 总体完成度保守估算约 92%,仍非 release-ready。
|
||||
```
|
||||
|
||||
### 9.73 V2-05 typed error retry/terminal closure
|
||||
|
||||
```text
|
||||
状态:completed(2026-07-31);只关闭 typed error retry/terminal 垂直切片,V2-05 与 v2 总体仍为 in-progress,不是 release-ready。
|
||||
|
||||
主目标:在唯一产品链内证明 error(willRetry=true) -> turn/completed(completed) 与 error(true)* -> error(false) -> turn/completed(failed);typed error 负责重试/错误可见性,权威 turn/completed 独占 Turn terminal,live、durable recovery、read model 和 GUI 使用同一 identity,生产 mock fallback 为零。
|
||||
|
||||
本轮窄写集:app-server-protocol v2 error notification/schema/generated client;App Server error projector 与 canonical read-model runtime notification merge;app-server-client typed decoder;agent-runtime-client signal/lifecycle 分流;Renderer strict typed error controller/presentation;Claw typed-error external fixture、assertion/evidence guards;EVENT-PROJECTIONS、refactor v2 实施计划与本执行计划。保留共享工作树其它 updater、provider、guardian、host capability 与 planned recovery 改动,不做无关改写。
|
||||
|
||||
退出条件:typed error 从 protocol/schema/generated client 到 Renderer strict decoder 一致;willRetry=true 不结束 Turn,willRetry=false 也必须等待权威 turn/completed;durable runtime.error 可由 canonical full/limited read model 恢复且不抢占后续 failed terminal;SDK signal 不污染 lifecycle sequence;定向 Rust/TS、contracts、Agent fixture 与 GUI smoke 通过;success/failure 两条 Gate B 证明真实 Electron/preload/IPC/app_server_handle_json_lines/App Server/RuntimeCore/read model/GUI、同一 identity 和零生产 mock fallback。
|
||||
|
||||
实现事实:App Server 把 durable runtime.error 投影成 v2 typed error;Renderer 只接受 typed event bus。willRetry=true 显示重试状态,后续 completed terminal 清理并完成;一个或多个 retry error 后的 willRetry=false 显示最终错误,仍由 failed terminal 决定 Turn。canonical read model 的 runtime notification merge 同时恢复 warning 与 error;runtime error 只形成可见错误证据,Turn 在权威 terminal 前保持 running。没有新增第二 persistence、第二 terminal reducer 或 raw agentSession/event wrapper。
|
||||
|
||||
SDK/fixture:agent-runtime-client 将 typed error 作为独立 signal,不送入 lifecycle sequence verifier。common assertion 将 provider first-text/provider-wait 与 App Server message delta、server timestamp、Renderer client output/paint、current trace method、W3C、GUI/read model/identity 分开;typed-error 场景只豁免不存在的 provider 首文本证据。新增回归锁定普通文本流仍要求 provider trace,typed-error 缺 App Server、Renderer 或 current trace 证据仍失败。
|
||||
|
||||
验证:typed-error Renderer/fixture 6 files / 94 tests、packages/app-server-client 8 files / 98 tests、packages/agent-runtime-client 22 tests 通过;Rust canonical runtime error recovery 与 runtime_error_does_not_preempt_a_later_turn_failed_terminal 两项定向回归分别通过。npm run test:contracts 通过,app-server-client contract 为 292 checks;npm run smoke:agent-runtime-current-fixture 完整通过且 liveProviderUsed=false;npm run verify:gui-smoke 21/21 assertions 通过,证据为 .lime/qc/project-gates/standalone-shell-01-20260731104641-30858/shell-01-electron-smoke/summary.json。
|
||||
|
||||
Gate B:success 场景观察 error(willRetry=true) -> turn/completed(completed),证据 .lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-typed-error-retry-success-gateb-02-summary.json;failure 场景观察 error(true)* -> error(false) -> pending read model -> turn/completed(failed) -> GUI/read-model/backend terminal,证据 .lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-typed-error-retry-failure-gateb-03-summary.json。两者 ok=true、proofLevel=Gate B controlled fixture;聚合回归 evidence 为同目录的 claw-chat-current-fixture-typed-error-retry-success-regression-summary.json 与 claw-chat-current-fixture-typed-error-retry-failure-regression-summary.json,均 ok=true。
|
||||
|
||||
架构确认:本切片没有改变 public owner、唯一产品链或依赖方向,只补齐既有 notification/projector/read-model/Renderer 边界的 typed contract 与恢复语义;无需改写 internal/aiprompts/architecture.md。责任开发者确认:root,2026-07-31。
|
||||
|
||||
治理分类:v2 typed error protocol/projector、durable runtime error recovery、strict Renderer signal 与权威 Turn terminal ownership 为 current;无 compat;raw error wrapper、provider trace 对 current 主链证据的错误替代和由 error 抢占 Turn terminal 的第二状态机为 dead/forbidden-to-restore;其余 V2-05 planned notification、host capability 与全面 recovery 仍为开放项。
|
||||
|
||||
OPEN_REF:下一刀回到 EVENT-PROJECTIONS.md,审计下一项具备真实 producer/consumer 的 planned notification、host capability 或 recovery;guardianWarning 在 Guardian runtime producer 落地前保持 planned。v2 总体完成度保守估算约 93%,仍非 release-ready。
|
||||
```
|
||||
|
||||
### 9.74 V2-05 `turn/plan/updated` canonical checklist 与 cold recovery closure
|
||||
|
||||
```text
|
||||
状态:completed(2026-07-31);只关闭 `turn/plan/updated` canonical checklist 与恢复垂直切片,V2-05 与 v2 总体仍为 in-progress,不是 release-ready。
|
||||
|
||||
主目标:在唯一产品链内把 canonical `update_plan` 的结构化输出派生为 typed `turn.plan.updated`,让实时和 canonical cold read 都显示同一份执行 checklist;保留 canonical `update_plan` 工具项用于恢复,但不生成 Plan ThreadItem、不展示 Plan UI/decision panel/tool card,也不新增 Message synthesis 或 renderer mock fallback。
|
||||
|
||||
本轮窄写集:App Server canonical `ToolOutput.structured_content` 到 `turn.plan.updated` 的 producer;Renderer strict notification parser;`appServerCanonicalThreadProjection` 的 update_plan cold recovery 与定向测试;App Server client contract guard;EVENT-PROJECTIONS、v2 实施计划与本执行计划。保留共享工作树其它 provider、warning、skills/changed、typed error、host capability 与 planned recovery 热区,不做无关改写。
|
||||
|
||||
退出条件:实时通知只接受 typed `turn.plan.updated`;冷读从成功 `update_plan` 的最新有效快照恢复;真实 App Server wire `[ { name: "plan", value: "[...]" } ]` 可解析;空数组可清空旧 checklist,失败/非法快照不覆盖;read model 只保留 canonical `update_plan` tool item;真实 Electron Gate B 证明实时与 reload 可见、Plan Item/Plan UI/decision panel/tool card 为零、同一 identity 与零 invoke/console error。
|
||||
|
||||
实现事实:App Server 从 canonical `ToolOutput.structured_content` 派生 `turn.plan.updated`,Renderer parser 严格消费 `turn_plan_updated`;`canonicalTurnPlanItems` 扫描 completed/successful `update_plan` tool item,按最新有效快照投影 `{ content, status }`,同时支持对象参数与真实 wire 参数数组。Malformed JSON、重复/缺失 `plan` 参数、非允许 status、失败或非 completed tool 均 fail closed;没有 Plan ThreadItem 或第二 checklist owner。
|
||||
|
||||
验证:`appServerCanonicalThreadProjection.test.ts` 10/10;`npm run test:contracts` 通过,app-server-client contract 为 292 checks;`npm run typecheck`、`npm run smoke:agent-runtime-current-fixture` 与 `npm run verify:gui-smoke` 通过。Gate B 证据 `.lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-summary.json` 的 `scenario=turn-plan-update`、`proofLevel=Gate B controlled fixture`、`ok=true`:`turn.plan.updated` `methodCount=1`,事件 drain `messageCount=28`、`planIndex=16`、`terminalIndex=27`;实时和 reload checklist 各有 2 项,`updatePlanToolCount=1`、`planItemCount=0`,`visiblePlanBlockCount=0`、`visiblePlanDecisionCount=0`、`visibleUpdatePlanToolRowCount=0`,Electron/preload/IPC/App Server/read model/GUI identity 一致,invoke/page/console error 均为 0。
|
||||
|
||||
治理分类:typed `turn.plan.updated` producer/parser、canonical `update_plan` recovery 与 run-control checklist 为 current;无 compat;`update_plan` 场景下的 Plan ThreadItem、Message/tool card checklist synthesis、Plan UI/decision panel/tool card 和 renderer mock fallback 为 dead/deleted/forbidden-to-restore;独立 Plan mode 的现役 UI 不在本切片范围内。
|
||||
|
||||
架构确认:本切片没有改变 public owner、唯一产品链或依赖方向,无需改写 `internal/aiprompts/architecture.md`。责任开发者确认:root,2026-07-31。
|
||||
|
||||
OPEN_REF:下一刀回到 EVENT-PROJECTIONS.md,审计下一项具备真实 producer/consumer 的 planned notification、host capability 或 recovery;guardianWarning 在 Guardian runtime producer 落地前保持 planned。v2 总体完成度保守估算约 94%,仍非 release-ready。
|
||||
```
|
||||
|
||||
### 9.75 V2-05 MCP OAuth typed completion notification closure
|
||||
|
||||
```text
|
||||
状态:completed(2026-07-31);只关闭 mcpServer/oauthLogin/completed 垂直切片,V2-05 与 v2 总体仍为 in-progress,不是 release-ready。
|
||||
|
||||
主目标:把 MCP OAuth callback completion 从旧 Desktop event 收敛到 App Server v2 typed notification,使成功/失败、Renderer 自动刷新与 GUI 可见状态走唯一产品链;不保留 mcp:oauth_completed compat、不新增第二 OAuth 状态 owner或生产 mock fallback。
|
||||
|
||||
本轮窄写集:lime-mcp OAuth completion handle;App Server MCP notification producer;app-server-protocol v2 notification/schema/generated client;app-server-client strict decoder;Renderer useMcp typed event subscription 与回归;MCP OAuth 专用 Electron fixture;contract guard;MCP/V2 文档与本执行计划。保留共享工作树其他 V2-05 notification、host capability、recovery 与版本改动,不做无关重写。
|
||||
|
||||
退出条件:callback success/failure 都产生 mcpServer/oauthLogin/completed;name 非空、threadId 必填可空、success 必填、error 可选可空且 decoder fail closed;Renderer 在任何 await 前同步订阅 typed event bus,完成后自动刷新 server/tool;生产路径无 mcp:oauth_completed;Rust/TS/contracts/governance/scripts 通过;真实 Electron Gate B 证明 preload/IPC、app_server_handle_json_lines、App Server event drain、OAuth callback、GUI 授权状态/toast 与零 mock fallback。
|
||||
|
||||
实现事实:lime-mcp 返回 McpOAuthLoginHandle completion future,不再发 legacy OAuth event。App Server 等待 handle 后把 success/error 投影为 ServerNotification::McpServerOauthLoginCompleted;protocol、schema、generated TypeScript 与 app-server-client strict decoder 使用同一 payload。Renderer setupMcpEventListeners 在串行 Desktop safeListen 前同步注册 App Server notification,避免快速 callback 竞态;成功/失败都刷新 mcpServerStatus/list 与 mcpTool/list,成功显示 completion toast,失败显示 typed error。
|
||||
|
||||
验证:lime-mcp OAuth 22 passed、app-server-protocol v2 59 passed、App Server MCP processor 4 passed、app-server-client 101 passed、useMcp 9 passed。npm run test:contracts 通过,包含 847 schema definitions、839 generated protocol types、0 generation failures、292 app-server-client checks,以及 command/scripts/docs/legacy guards。
|
||||
|
||||
Gate B:npm run smoke:mcp-oauth-notification-electron-fixture 通过,proofLevel=Gate B、ok=true。真实 Electron/preload/IPC、app_server_handle_json_lines、App Server OAuth login/event drain、本地 provider redirect/callback/token exchange 与 Renderer GUI 全部命中;oauthRequiredVisible=true、oauthAuthorizedVisible=true、completionToastVisible=true,authorizeRequestCount=1、tokenRequestCount=1、scope=fixture.read,appServerHandleJsonLinesHitCount=3、appServerDrainEventsHitCount=4、openExternalUrlHitCount=1、electronIpcHitCount=8、mockFallbackHitCount=0、failedInvokeCount=0,console/page/invoke errors 为 0。自动刷新 methods 为 mcpServer/oauth/login、mcpServerStatus/list、mcpTool/list。证据:.lime/qc/mcp-oauth-notification/mcp-oauth-notification-fixture-summary.json 与同目录 PNG。
|
||||
|
||||
架构确认:本切片没有改变 public owner、唯一产品链或依赖方向,只把既有 OAuth completion 从 legacy Desktop event 迁到 App Server v2 typed notification,并补齐 Renderer/Gate B;无需改写 internal/aiprompts/architecture.md。责任开发者确认:root,2026-07-31。
|
||||
|
||||
治理分类:typed protocol/schema、App Server completion producer、strict client decoder、Renderer typed event bus、自动刷新与专用 Gate B 为 current;无 compat;mcp:oauth_completed 为 dead/deleted/forbidden-to-restore。
|
||||
|
||||
OPEN_REF:下一刀回到 EVENT-PROJECTIONS.md,继续审计具备真实 producer/consumer 的 planned notification、host capability 或 recovery;guardianWarning 在 Guardian runtime producer 落地前保持 planned。v2 总体完成度保守估算约 95%,仍非 release-ready。
|
||||
```
|
||||
|
||||
## 10. 完成定义
|
||||
|
||||
v2 实施不能因为“几个进程都有改动”而标记完成。每个 slice 必须同时具备:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Lime v1.118.0 发布执行计划
|
||||
|
||||
状态:completed
|
||||
日期:2026-08-01
|
||||
目标版本:`1.118.0`
|
||||
目标 tag:`v1.118.0`
|
||||
|
||||
## 主目标
|
||||
|
||||
将 `v1.117.0` 后当前工作树中的 Skills catalog 自动失效、typed error、`turn/plan/updated`、MCP OAuth 完成通知、Windows Provider 模型目录与 updater 可靠性,以及核心用户流程修复作为同一个 release candidate,完成版本事实源、双语 release notes、发布门禁、release commit、tag、`main`/tag 推送与远端复核。
|
||||
|
||||
## 当前阶段与下一刀
|
||||
|
||||
- 当前阶段:`v1.118.0` release candidate、版本事实源、双语 release notes、发布门禁、release commit、tag、`main`/tag 推送与远端引用复核均已完成。
|
||||
- 下一刀:无;后续增量进入下一版本,不改写已发布的 `v1.118.0` tag。
|
||||
|
||||
## Release Candidate
|
||||
|
||||
- 基线:`v1.117.0`。
|
||||
- 最终候选盘点:167 个 tracked diff 文件、30 个未跟踪文件,tracked diff 为 12,614 行新增、1,563 行删除。
|
||||
- `release metadata`:根 `package.json`、`packages/lime-cli-npm/package.json`、`lime-rs/Cargo.toml`、`lime-rs/Cargo.lock`、双语 release notes 与本计划。
|
||||
- `candidate changes`:当前工作树全部产品、协议、文档、测试、schema、生成物和脚本改动;重点包括 Skills catalog invalidation、typed error retry/terminal、canonical plan checklist、MCP OAuth typed notification、Provider model catalog 和 updater 状态修复。
|
||||
- `excluded changes`:无。初始盘点未发现缓存、构建产物、凭证、个人临时文件或与候选无关的大文件。
|
||||
- 并行避让:本轮除 release metadata、本计划和 Rust 门禁暴露的一处过时 fixture 外,其余候选文件只读验证,不覆盖已有业务改动。门禁期间新增的 MCP 架构/进度文档属于同一 current notification 候选,已纳入最终盘点与后续验证。
|
||||
|
||||
## 写集与退出条件
|
||||
|
||||
- 所有版本事实源同步为 `1.118.0`,release notes 只保留当前版本单页。
|
||||
- 必跑 `npm run verify:app-version`、`npm run typecheck`、`npm run test:contracts`、`npm run test:rust:changed`、`npm run smoke:agent-runtime-current-fixture`、`npm run verify:gui-smoke` 与 `git diff --check`。
|
||||
- 协议、App Server、Electron、GUI、脚本与重大架构均有变化;门禁失败必须定位修复或记录明确阻塞,不能静默降级。
|
||||
- 重大架构变更已同步 `internal/aiprompts/architecture.md`,并包含责任开发者确认。
|
||||
- Git 写操作已获得明确确认;完整 release candidate 由同一 release commit 承载,`v1.118.0` tag、远端 `main` 与远端 tag 指向同一发布对象。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- `npm run verify:app-version`:版本更新后与最终复跑均通过,根应用、CLI npm 包、Rust workspace 与锁文件一致为 `1.118.0`。
|
||||
- `npm run typecheck`:通过,覆盖 renderer 与 Node TypeScript 配置。
|
||||
- `npm run test:contracts`:通过;839 个 generated protocol types、0 generation failure、292 项 App Server client contract,以及 command、harness、modality、scripts、Electron release workflow 和 docs boundary guard 全部通过。
|
||||
- `npm run test:rust:changed`:因 workspace 版本和 lockfile 变化扩大为 `cargo test --lib --workspace`。首次运行发现 `maps_item_and_terminal_lifecycle_to_direct_v2` 仍使用旧 `turn.failed` fixture;更新为当前 `error -> turn/completed` contract 后,定向测试 1/1 与最终 workspace 全部 suite 通过,关键结果包括 App Server 1650/1650、protocol 98/98、MCP 151/151,0 failed;既有联网 embedding 测试按规则 ignored。
|
||||
- `cargo fmt --manifest-path "lime-rs/Cargo.toml" --all --check` 与 `git diff --check`:通过。
|
||||
- `npm run smoke:agent-runtime-current-fixture`:通过;覆盖 history/cache hydration、停止继续、approval、Skills catalog 自动刷新、typed error retry success/failure、Plan 历史恢复、MCP structuredContent、media、Coding Workbench 与 Article Editor,`liveProviderUsed=false`。
|
||||
- `npm run verify:gui-smoke`:通过;App Server 报告 `version=1.118.0`,真实 Electron Gate B 摘要位于 `.lime/qc/project-gates/standalone-shell-01-20260731155713-55975/shell-01-electron-smoke/summary.json`。
|
||||
- `npm run smoke:mcp-oauth-notification-electron-fixture`:通过;真实 Electron、外部浏览器回调与 App Server v2 notification 闭环命中 `mcpServer/oauthLogin/completed`,证据位于 `.lime/qc/mcp-oauth-notification/mcp-oauth-notification-fixture-summary.json`。
|
||||
- 工作树稳定性:2026-08-01 00:01:15 与 00:01:34 两次采样的 status、tracked diff、untracked content 三组 SHA-256 完全一致;最终候选保持 167 个 tracked diff 文件与 30 个未跟踪文件。本地 `HEAD`、`origin/main` 与远端 `main` 均为 `66752a277`,目标 tag 仍不存在。
|
||||
- Staged 边界:197 个候选文件全部暂存,摘要为 16,356 行新增、1,563 行删除;未暂存与未跟踪文件均为 0,`git diff --cached --check` 通过。
|
||||
- 发布结果:release commit、`v1.118.0` tag、`main`/tag 推送与远端引用复核完成;发布后工作树 clean。
|
||||
- 平台限制:Windows Squirrel packaged Gate B 尚未执行;macOS Electron fixture 不能替代 Windows 实机证据。
|
||||
|
||||
## 分类
|
||||
|
||||
- `current`:Electron Desktop Host -> App Server JSON-RPC -> RuntimeCore -> Thread/Turn/Item -> GUI 产品链;typed v2 notifications、Skill catalog invalidation、canonical checklist/error recovery、Provider 模型目录和 Electron updater。
|
||||
- `compat`:无新增。
|
||||
- `deprecated`:Refactor v2 尚未迁完的 V2-05 notification、host capability 与 recovery surface,只允许继续迁出。
|
||||
- `dead / deleted / forbidden-to-restore`:raw notification wrapper、第二 catalog/checklist/error owner、Provider inferred-only 可执行假设、updater 伪版本判断与生产 mock fallback。
|
||||
|
||||
## 完成度
|
||||
|
||||
- 当前完成度:100%。候选范围、发布元数据、发布门禁、staged 复核、commit、tag、push 与远端复核均已完成。
|
||||
@@ -0,0 +1,67 @@
|
||||
# Windows 模型目录与更新可靠性闭环
|
||||
|
||||
状态:实现完成;Windows Gate B 待实机验收
|
||||
日期:2026-07-31
|
||||
|
||||
## 目标
|
||||
|
||||
1. Windows 用户添加并启用 Provider 模型后,聊天模型选择器必须展示该显式模型。
|
||||
2. 更新入口只在确认候选版本严格高于当前版本后出现。
|
||||
3. 更新失败、已是最新和发现新版本使用不同状态,不再把失败或同版本误报成新版本。
|
||||
|
||||
## 用户闭环
|
||||
|
||||
用户从设置中的 Provider 添加模型和凭证,回到聊天输入区打开模型选择器,能看到并选择刚添加的模型。应用后台检查更新时不打扰用户;只有已确认的新版本可安装时才显示更新入口,失败时提供准确状态和可恢复动作。
|
||||
|
||||
## 写集
|
||||
|
||||
- `src/components/input-kit/ModelSelector.tsx` 及定向测试
|
||||
- `src/components/agent/chat/utils/providerModelCompatibility.ts` 及定向测试
|
||||
- `electron/updateHost.ts` 及定向测试
|
||||
- `src/components/app-sidebar/AppUpdateEntry.tsx` 及定向测试
|
||||
- `src/pages/update-notification.tsx` 及定向测试
|
||||
- `src/i18n/resources/*/common.json` 的更新状态文案
|
||||
- 本执行记录
|
||||
|
||||
避让:App Server protocol、Skills runtime、`internal/aiprompts/architecture.md` 与已有脏执行计划。
|
||||
|
||||
## 根因证据
|
||||
|
||||
- 模型:Provider 显式模型会被前端临时投影为无权威能力来源的元数据,`ModelSelector` 又把该来源当成路由不可执行并直接过滤,形成“顶部保留旧选择、候选列表为空”。
|
||||
- 更新:`update-available` 事件没有版本参数,旧逻辑以 `undefined !== currentVersion` 判断有更新;通知页又固定渲染“发现新版本”,未按 `failed / up_to_date / completed` 分支。
|
||||
|
||||
## 退出条件
|
||||
|
||||
- Provider 显式模型可见,未显式声明且仅靠启发式推断的目录项仍 fail closed。
|
||||
- 同版本、旧版本、无版本和无效版本均不会产生可安装更新入口。
|
||||
- 只有严格更高版本进入已下载/可安装状态;失败通知不显示“发现新版本”。
|
||||
- 定向单测、`typecheck`、`test:contracts`、`verify:app-version` 和 `verify:gui-smoke` 完成或记录明确阻塞。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- 模型定向回归:`providerModelCompatibility.test.ts` 与 `ModelSelector.test.tsx`,48/48 通过。
|
||||
- 更新与模型联合定向回归:7 个测试文件,99/99 通过。
|
||||
- Electron 版本与宿主定向回归:23/23 通过。
|
||||
- `npm run typecheck`:通过。
|
||||
- `npm run typecheck:electron`:通过。
|
||||
- `npm run verify:app-version`:通过,当前版本 `1.117.0`。
|
||||
- `npm run test:contracts`:通过。
|
||||
- `npm run verify:gui-smoke`:通过;Electron、preload/IPC、App Server JSON-RPC 与用户可见工作台主路径 smoke 证据:`.lime/qc/project-gates/standalone-shell-01-20260731050343-95334/shell-01-electron-smoke/summary.json`。
|
||||
- 2026-07-31 最终 GUI smoke:21/21 assertions 通过,console / invoke / preload / page error 均为 0,mock fallback 与 legacy command 命中为 0;证据:`.lime/qc/project-gates/standalone-shell-01-20260731103342-78558/shell-01-electron-smoke/summary.json`。
|
||||
- updater / About / UpdateNotification / Sidebar 定向回归:51/51 通过;`npm run typecheck:electron` 与 `npm run verify:app-version` 通过,版本 `1.117.0`。
|
||||
- `npm run smoke:agent-runtime-current-fixture`:通过;Provider 模型选择后的 current Agent 主链、历史、停止继续、typed error success/failure 与 read model terminal 未出现回归,`liveProviderUsed=false`。
|
||||
- `npm run verify:local`:通过;前端 112/112 个智能测试批次、Rust workspace 测试和内含 GUI smoke 全部通过。
|
||||
- 线上 Windows x64 `RELEASES` 清单:本机到 `updates.limecloud.com` 连续三次 TLS 握手失败,未取得服务端内容。
|
||||
- 待实机验收:Windows Squirrel 从 N-1 版本发现更新、下载、重启安装并确认最终版本的 Gate B 闭环。
|
||||
- 当前完成度:92%;实现、macOS Electron Gate B 与跨层回归已完成,剩余 8% 为 Windows packaged 平台验收,不能由 macOS fixture 代替。
|
||||
|
||||
## 状态分类
|
||||
|
||||
- current:Provider 配置 -> 模型目录 -> 模型选择器;Electron `autoUpdater` -> 更新会话 -> GUI。
|
||||
- compat:无新增。
|
||||
- deprecated:无新增。
|
||||
- dead / deleted:删除 `undefined !== currentVersion` 伪版本判断,以及版本未知时的用户可见更新入口。
|
||||
|
||||
## 架构确认
|
||||
|
||||
未改变 Electron Desktop Host、preload/IPC 或 App Server 边界,不属于重大架构变更,无需更新架构图。
|
||||
@@ -13,20 +13,20 @@
|
||||
|
||||
出口代码:TL 时间线,TP Turn 面板,PI pending interaction,HS Header/状态区,GN 应用通知,DX 仅开发诊断。
|
||||
|
||||
实施快照:direct `item/started -> item/commandExecution/outputDelta* -> item/completed` 通过 typed adapter 和共享 reducer;production `thread/resume` 安装同一 replay reducer,后续 live notification 继续复用。completed snapshot 权威覆盖 delta 草稿,输出限制为 256 KiB。unknown Item 与 unknown/known-unprojected notification drift recorder 已接线,但 recorder 只提供 fail-visible 诊断,不能把 72 notification 中的 planned surface 标记完成。
|
||||
实施快照:direct `item/started -> item/commandExecution/outputDelta* -> item/completed` 通过 typed adapter 和共享 reducer;production `thread/resume` 安装同一 replay reducer,后续 live notification 继续复用。completed snapshot 权威覆盖 delta 草稿,输出限制为 256 KiB。`turn.plan.updated` 由 canonical `update_plan` 的 `ToolOutput.structured_content` 派生,实时与 canonical cold read 共用 checklist 投影;`update_plan` 工具项保留在 read model,但不生成 `ThreadItem.plan` 或 Plan UI。unknown Item 与 unknown/known-unprojected notification drift recorder 已接线,但 recorder 只提供 fail-visible 诊断,不能把 72 notification 中的 planned surface 标记完成。
|
||||
|
||||
## 1. Thread、Turn 与 Hook
|
||||
|
||||
| # | Method | 目标出口 | 当前裁决 | v2 投影 |
|
||||
| --: | ------------------------------- | -------- | -------- | ----------------------------------------------------- |
|
||||
| 1 | error | TP/HS | planned | thread/turn 错误;willRetry 只显示重试中 |
|
||||
| 1 | error | TP/HS | current | typed live/durable;true 重试,false 等权威 Turn |
|
||||
| 2 | thread/started | HS | current | 建立 Thread metadata,不把空 turns 当完整 history |
|
||||
| 3 | thread/status/changed | HS/PI | current | notLoaded、idle、systemError、active 与 waiting flags |
|
||||
| 4 | thread/archived | GN/HS | current | 侧栏归档,当前页只读 |
|
||||
| 5 | thread/deleted | GN | current | 当前页已删除态,不删除 Project |
|
||||
| 6 | thread/unarchived | GN/HS | current | 恢复可见/可操作状态 |
|
||||
| 7 | thread/closed | HS | current | 清除 live spinner 和 pending interaction |
|
||||
| 8 | skills/changed | GN | planned | 刷新 Composer Skill catalog |
|
||||
| 8 | skills/changed | GN | current | 瞬时失效并重新读取 Composer current Skill catalog |
|
||||
| 9 | thread/name/updated | HS | current | Header 与侧栏名称 |
|
||||
| 10 | thread/goal/updated | HS | current | goal/阶段入口,不映射 Rust ProductionPlan |
|
||||
| 11 | thread/goal/cleared | HS | current | 清除当前 goal indicator |
|
||||
@@ -39,7 +39,7 @@
|
||||
| 18 | turn/completed | TL/TP | current | 权威 Turn 终态并清理 pending |
|
||||
| 19 | hook/completed | TL | planned | Hook status、duration、entries 与阻断结果 |
|
||||
| 20 | turn/diff/updated | TP | planned | 替换聚合 Diff snapshot |
|
||||
| 21 | turn/plan/updated | TP | planned | 执行 checklist,不等同 plan Item |
|
||||
| 21 | turn/plan/updated | TP | current | canonical update_plan checklist,实时/冷恢复一致 |
|
||||
|
||||
## 2. Item 生命周期、流与进程
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
| # | Method | 目标出口 | 当前裁决 | v2 投影 |
|
||||
| --: | ------------------------------------ | -------- | ---------------------- | ---------------------------------------- |
|
||||
| 39 | mcpServer/oauthLogin/completed | GN/PI | planned | 结束 OAuth 等待并刷新 MCP status |
|
||||
| 39 | mcpServer/oauthLogin/completed | GN/PI | current | 结束 OAuth 等待并刷新 MCP status |
|
||||
| 40 | mcpServer/startupStatus/updated | HS/GN | planned | starting/ready/failed,无假 MCP fallback |
|
||||
| 41 | account/updated | GN | product-scope-excluded | 不写入对话 history |
|
||||
| 42 | account/rateLimits/updated | HS/GN | product-scope-excluded | 若未来纳入,独立产品范围变更 |
|
||||
@@ -167,7 +167,7 @@ command/file approval、requestUserInput、MCP elicitation 与 permission approv
|
||||
|
||||
## 9. 非 Item 投影与状态终结
|
||||
|
||||
Turn plan 是执行 checklist,不是 ThreadItem.plan 或 Rust ProductionPlan。Turn diff 是本 Turn 聚合 Diff,不能取代 fileChange Item。Header 只显示当前 model、reasoning、permission/sandbox、environment 与 active flags 的用户级摘要;底层 route、provider、credential 和 capability 明细进入受控运行详情。
|
||||
Turn plan 是 canonical `update_plan` 的执行 checklist,不是 ThreadItem.plan 或 Rust ProductionPlan;实时 `turn.plan.updated` 与 cold read 从同一结构化快照恢复,非法快照不覆盖上一份有效计划。Turn diff 是本 Turn 聚合 Diff,不能取代 fileChange Item。Header 只显示当前 model、reasoning、permission/sandbox、environment 与 active flags 的用户级摘要;底层 route、provider、credential 和 capability 明细进入受控运行详情。
|
||||
|
||||
| 触发 | 必须终结的 UI |
|
||||
| --------------------------- | ----------------------------------------------------- |
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
主目标:以 Codex 的 Thread / Turn / Item 生命周期和渲染语义替换 Lime 当前把 canonical Item 二次压缩为 Message、tool_call、extension 的对话渲染路径,同时保持 model-provider 的 Grok 多模型控制面和 OpenCode 多模态/多协议 lowering 不变。
|
||||
|
||||
当前阶段:V2-05 notification、host capability 与 recovery;V2-00、V2-01、V2-02、V2-03、V2-04 已关闭,direct TurnTimeline、production replay、session history、长列表性能、MCP elicitation、Multi-Agent、三项 product-scope reverse request 与 media read v2 Gate B 已通过
|
||||
当前阶段:V2-05 notification、host capability 与 recovery;V2-00、V2-01、V2-02、V2-03、V2-04 已关闭,direct TurnTimeline、production replay、session history、长列表性能、MCP elicitation、Multi-Agent、三项 product-scope reverse request、media read v2、`skills/changed` catalog invalidation、typed `error` retry/terminal、`turn/plan/updated` checklist 与 `mcpServer/oauthLogin/completed` Gate B 已通过
|
||||
|
||||
下一刀:V2-05 已关闭 media transient bypass、`configWarning` owner 迁移与 thread-scoped `warning` typed/recovery 链;继续审计 `skills/changed` 等具备真实 producer 的 notification,`guardianWarning` 在 Guardian runtime producer 落地前保持 planned。不重复改写已关闭的 V2-04 owner,也不恢复 Message synthesis、extension fallback、v0 media/config owner、旧 Team 工具或第二 pending store。
|
||||
下一刀:V2-05 已关闭 media transient bypass、`configWarning` owner 迁移、thread-scoped `warning` typed/recovery、`skills/changed` catalog invalidation、typed `error` retry/terminal、`turn/plan/updated` checklist 与 MCP OAuth typed completion 链;回到 `EVENT-PROJECTIONS.md` 审计下一项具备真实 producer/consumer 的 planned notification、host capability 或 recovery,`guardianWarning` 在 Guardian runtime producer 落地前保持 planned。不重复改写已关闭的 V2-04 owner,也不恢复 Message synthesis、extension fallback、v0 media/config owner、旧 MCP OAuth Desktop event、旧 Team 工具、第二 pending store、第二 Skill catalog owner、Plan ThreadItem 或由 error 抢占 Turn terminal 的旁路状态机。
|
||||
|
||||
## 1. 约束与非目标
|
||||
|
||||
@@ -252,14 +252,14 @@ Gate B 最少场景:Agent Markdown、Reasoning、Plan、Search、Shell output
|
||||
|
||||
## 5. 分类与完成度
|
||||
|
||||
| 分类 | v2 裁决 |
|
||||
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| current | App Server v2 主链、ConversationProjection、direct TurnTimeline、bounded restored Turn window、canonical long-message preview、production replay、统一 PendingInteraction、thread-scoped media read、typed `configWarning` producer/GUI、thread-scoped typed `warning` 与 durable recovery、六工具 AgentControl、canonical SubAgent activity、parent-owned child direct-input policy、`currentTime/read`、`item/permissions/requestApproval`、`item/tool/call`、typed DynamicToolCall、model-provider 多模型/多模态 owner |
|
||||
| compat | 不保留长期生产 compat;仅可存在一次性迁移测试夹具 |
|
||||
| deprecated | fileChange outputDelta、thread/compacted 与尚未完成 current producer 的旧 notification 裁决;只允许迁出 |
|
||||
| dead / deleted | canonical Item -> Message tool/agent/reasoning 合成、首帧无界历史挂载、canonical 长正文绕过 preview、unknown Item null drop、ContextCompaction hide、通用 extension fallback、重复 pending store、v0 media/config notification owner、media transient notification/raw subscription/live-drain 旁路、裸旧 Team 工具、raw output 状态推断与生产 mock fallback |
|
||||
| 分类 | v2 裁决 |
|
||||
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| current | App Server v2 主链、ConversationProjection、direct TurnTimeline、bounded restored Turn window、canonical long-message preview、production replay、统一 PendingInteraction、thread-scoped media read、typed `configWarning` producer/GUI、thread-scoped typed `warning` 与 durable recovery、typed `skills/changed` catalog invalidation 与 GUI 自动重读、typed `error` live/durable recovery 与 Turn terminal ownership、`turn/plan/updated` canonical checklist 与 cold recovery、typed `mcpServer/oauthLogin/completed` 与 GUI 自动刷新、六工具 AgentControl、canonical SubAgent activity、parent-owned child direct-input policy、`currentTime/read`、`item/permissions/requestApproval`、`item/tool/call`、typed DynamicToolCall、model-provider 多模型/多模态 owner |
|
||||
| compat | 不保留长期生产 compat;仅可存在一次性迁移测试夹具 |
|
||||
| deprecated | fileChange outputDelta、thread/compacted 与尚未完成 current producer 的旧 notification 裁决;只允许迁出 |
|
||||
| dead / deleted | canonical Item -> Message tool/agent/reasoning 合成、首帧无界历史挂载、canonical 长正文绕过 preview、unknown Item null drop、ContextCompaction hide、通用 extension fallback、重复 pending store、v0 media/config notification owner、media transient notification/raw subscription/live-drain 旁路、旧 MCP OAuth Desktop event、裸旧 Team 工具、raw output 状态推断与生产 mock fallback |
|
||||
|
||||
计划完成度:保守估算 91%。该数字按 V2-00 至 V2-05 六个阶段退出条件与剩余风险折算:V2-00、V2-01、V2-02、V2-03、V2-04 已关闭,V2-05 的 media transient bypass、`configWarning` typed owner 和 thread-scoped `warning` typed/recovery 链已关闭;其余 planned notification、host capability 与全面 recovery 仍未完成。该估算不表示 v2 已可整体交付或 release-ready。
|
||||
计划完成度:保守估算 95%。该数字按 V2-00 至 V2-05 六个阶段退出条件与剩余风险折算:V2-00、V2-01、V2-02、V2-03、V2-04 已关闭,V2-05 的 media transient bypass、`configWarning` typed owner、thread-scoped `warning` typed/recovery、`skills/changed` catalog invalidation、typed `error` retry/terminal、`turn/plan/updated` canonical checklist 与 MCP OAuth typed completion 链已关闭;其余 planned notification、host capability 与全面 recovery 仍未完成。该估算不表示 v2 已可整体交付或 release-ready。
|
||||
|
||||
## 6. 执行台账
|
||||
|
||||
@@ -631,10 +631,90 @@ Gate B 证据:
|
||||
- full canonical 冷读与 history-limit projection summary 都从同一 durable warning event 派生历史 warning item,并保留 `code`;没有新增第二 persistence 或 canonical Item lifecycle 变体。
|
||||
- 实时与恢复都只接受精确 message/code shape;畸形 message/code、旧 alias、缺失或不匹配的 thread/turn identity 均 fail closed。raw `agentSession/event` 包装的 `runtime.warning` 被拒绝。`guardianWarning` 因没有 Guardian runtime producer 继续保持 planned。
|
||||
|
||||
验证:protocol v2 50/50、schema fixture 1/1、App Server warning 12/12、projection-summary recovery 1/1、Renderer 3 files / 61 tests、generated TypeScript drift check 均通过。`npm run test:contracts` 通过 289 项 app-server-client checks 及全部附属 guards;Rust related 覆盖 19 个 scoped packages、18 个 lib 目标,共 3597 项(3593 passed、4 个既有环境/live 测试 ignored、0 failed)。`npm run smoke:agent-runtime-current-fixture` 完整通过且 `liveProviderUsed=false`;该聚合 smoke 没有普通 warning 专项 DOM 断言,不冒充 warning 专项 Gate B。通用 `npm run verify:gui-smoke` 21/21 assertions 通过,证据为 `.lime/qc/project-gates/standalone-shell-01-20260730191439-55464/shell-01-electron-smoke/summary.json`。`cargo fmt --all --check`、定向 Prettier、`git diff --check` 与 `npm run governance:legacy-report` 通过,治理结果为零引用候选 0、分类漂移 0、边界违规 0。
|
||||
验证:protocol v2 50/50、schema fixture 1/1、App Server warning 12/12、projection-summary recovery 1/1、Renderer 3 files / 61 tests、generated TypeScript drift check 均通过。`npm run test:contracts` 通过 289 项 app-server-client checks 及全部附属 guards;Rust related 覆盖 19 个 scoped packages、18 个 lib 目标,共 3597 项(3593 passed、4 个既有环境/live 测试 ignored、0 failed)。`npm run smoke:agent-runtime-current-fixture` 完整通过且 `liveProviderUsed=false`;该聚合 smoke 没有普通 warning 专项 DOM 断言,不冒充 warning 专项 Gate B。fresh `npm run verify:local` 通过 i18n、lint、typecheck、112/112 Vitest、contracts、Rust changed-scope、Electron renderer/host build 与真实 GUI smoke;后者 21/21 assertions 通过,证据为 `.lime/qc/project-gates/standalone-shell-01-20260730194114-24572/shell-01-electron-smoke/summary.json`。`cargo fmt --all --check`、定向 Prettier、`git diff --check` 与 `npm run governance:legacy-report` 通过,治理结果为零引用候选 0、分类漂移 0、边界违规 0。
|
||||
|
||||
治理分类:v2 `warning` protocol/projector、thread-scoped Renderer toast 与 durable recovery 为 `current`;无 `compat`;raw warning wrapper 为 `dead / forbidden-to-restore`;`guardianWarning` 为 `planned`。`v2_notifications.rs` 已超过 1000 行且主要由 inline tests 构成,本刀只增加路由并把业务 projector 放入独立 `warning.rs`;后续触碰 inline test 区前应迁出专用测试模块。
|
||||
|
||||
架构确认:typed notification/recovery owner 已同步 `internal/aiprompts/architecture.md` 第 27 节;唯一产品链保持不变。责任开发者确认:root,2026-07-31。
|
||||
|
||||
下一刀:审计 `skills/changed` 与其真实 Skill catalog producer/consumer;只有能形成 typed producer -> GUI refresh -> Gate B 的垂直切片才改为 current。v2 总体完成度保守估算约 91%。
|
||||
|
||||
### 2026-07-31:V2-05 `skills/changed` catalog invalidation
|
||||
|
||||
状态:该垂直切片 `completed`;V2-05 与 v2 总体仍为 `in-progress`,不得标记 release-ready。
|
||||
|
||||
本轮实现:
|
||||
|
||||
- v2 protocol、schema、generated client 与 typed decoder 只接受 `skills/changed {}`;默认 Skill roots 的创建、修改、删除以及启动后首次出现都经 10 秒节流 watcher 失效 `lime-skills` snapshot cache 并广播通知。
|
||||
- 成功的 Lime catalog mutation 在 App Server processor 边界失效同一 cache 并附带 typed notification;失败 mutation 和其他 app 的 mutation 不发通知。
|
||||
- Composer `useLimeSkills` 的自动加载与 notification refresh 统一读取 current `skill/list`;管理中心 `skillManagement/list` 语义不变,`skillsApi.getRuntimeCatalog()` 复用既有 executable Skill decoder 与纯 catalog projection,不新增第二 catalog owner。
|
||||
- Renderer 只订阅 typed event bus;命中后记录 console-only `skillsChanged.received` marker 并自动重读,重连/重新挂载仍主动 list。该 notification 是进程级瞬时失效,不写入 Thread/Turn/Item、持久化或 recovery replay。
|
||||
|
||||
验证:纯 projection、API、Hook 与 fixture guard 共 4 files / 99 tests 通过;`npm run typecheck`、`npm run test:contracts`(840 schema definitions、832 generated types、0 generation failures、290 app-server-client checks)、`npm run governance:legacy-report`、Prettier、MJS `node --check`、`cargo fmt --all --check` 与 `git diff --check` 通过。Rust workspace lib 汇总为 32 个 suite、4670 passed、5 ignored、0 failed。`npm run test:related` 因仓库既有 runner 把 `electron/` 目录当文件读取而报 `EISDIR`,未记为通过,已由精确 Vitest 覆盖本轮前端回归。
|
||||
|
||||
产品证据:`npm run smoke:claw-chat-current-fixture -- --scenario skills-runtime` 通过,proof level 为 `Gate B controlled fixture`。真实 Electron/preload/IPC/App Server 链先产生初始 `skill/list`,随后隔离 HOME 默认 root 新建 `notification-refresh/SKILL.md`,typed `skills/changed` marker 为 1,`skill/list` 自动由 1 次增至 2 次且 transport 为 `electron-ipc`,GUI 新 Skill 可见,手动刷新点击为 0;console/page/actionable errors 与 failed assertions 均为 0。证据:`.lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-summary.json`。
|
||||
|
||||
整合复验:聚合 `npm run smoke:agent-runtime-current-fixture` 完整通过且 `liveProviderUsed=false`,包含 `skills/changed`、Inputbar rich restore、Skills Runtime 三入口及其余 current Agent/Electron fixture。首次聚合运行暴露的 Inputbar 失败属于 fixture DOM 定位合同:夹具用展示名 `Capability Report` 大小写敏感匹配 current catalog 的 `capability-report`,因此候选已可见但未点击;生产 CharacterMention 与 catalog identity 无缺陷。夹具现统一折叠大小写、空格、连字符和下划线,并用 source guard 锁定候选与 `@capability-report` badge 的同一等价规则。独立 `inputbar-rich-restore` Gate B 随后通过,text/image/path/skill 在 output-free cancel 后全部恢复,console/page error 与 mock fallback 为 0;证据为 `.lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-inputbar-rich-restore-regression-summary.json`。
|
||||
|
||||
通用 `npm run verify:gui-smoke` 通过,真实 Electron renderer、App Server sidecar、Claw shell reload 与 memory settings 均就绪;证据为 `.lime/qc/project-gates/standalone-shell-01-20260731043417-63564/shell-01-electron-smoke/summary.json`。fresh `npm run verify:local` 通过版本一致性和 i18n unused 后,在全仓 lint 被非本切片脏文件 `src/components/input-kit/ModelSelector.tsx:432` 的既有 `react-hooks/exhaustive-deps` warning 阻断,因此未记为通过;本切片未改该文件或越界修复。后续独立 `npm run typecheck`、contracts、99 项精确测试、GUI smoke、Rust fmt 与 Gate B 均已取得明确通过结果。
|
||||
|
||||
治理分类:v2 `skills/changed` protocol/schema、App Server watcher/mutation producer、`lime-skills` cache invalidation、typed Renderer event bus 与 current `skill/list` GUI refresh 为 `current`;无 `compat`;`skillManagement/list` 仅保留管理中心原语义,不是 Composer catalog owner;持久化/replay、`skills/extraRoots/set`、第二 catalog owner 与生产 mock fallback 均未新增。
|
||||
|
||||
架构确认:catalog invalidation owner 已同步 `internal/aiprompts/architecture.md` 第 28 节;唯一产品链保持不变。责任开发者确认:root,2026-07-31。
|
||||
|
||||
下一刀:回到 `EVENT-PROJECTIONS.md` 审计下一项具备真实 producer/consumer 的 planned notification、host capability 或 recovery;`guardianWarning` 在 Guardian runtime producer 落地前保持 planned。v2 总体完成度保守估算约 92%。
|
||||
|
||||
### 2026-07-31:V2-05 typed `error` retry/terminal closure
|
||||
|
||||
状态:该垂直切片 `completed`;V2-05 与 v2 总体仍为 `in-progress`,不得标记 release-ready。
|
||||
|
||||
本轮实现:
|
||||
|
||||
- v2 protocol、schema、generated client 与 strict Renderer decoder 承接 typed `error`;App Server projector 将 durable `runtime.error` 投影到同一 thread/turn identity,raw `agentSession/event` 不再是 GUI owner。
|
||||
- `willRetry=true` 只进入可见重试状态,后续权威 `turn/completed(completed)` 清理提示并完成 Turn;一个或多个 retry error 后的 `willRetry=false` 只显示 terminal error,最终失败仍由权威 `turn/completed(failed)` 决定。
|
||||
- canonical full/limited read model 统一合入 durable runtime warning 与 error。恢复出的 runtime error 保留可见错误证据,但 Turn 保持 running,直到 durable Turn terminal 到达;没有新增第二 persistence、第二 terminal reducer 或 error 合成终态。
|
||||
- SDK 将 typed error 放在独立 signal 通道,不送入只接受 lifecycle sequence 的 verifier。Gate B assertion 把 provider first-text trace 与 App Server message delta、Renderer output/paint、current trace method、W3C 和 identity 证据拆分;typed-error 场景只豁免本来不会发生的 provider 首文本证据,不能豁免 current 主链证据。
|
||||
|
||||
验证:typed-error Renderer/fixture 共 6 files / 94 tests、`packages/app-server-client` 8 files / 98 tests、`packages/agent-runtime-client` 22 tests 通过;Rust canonical runtime error recovery 与 `runtime_error_does_not_preempt_a_later_turn_failed_terminal` 两项定向回归分别通过。`npm run test:contracts` 通过,app-server-client contract 为 292 checks;`npm run smoke:agent-runtime-current-fixture` 完整通过且 `liveProviderUsed=false`;`npm run verify:gui-smoke` 21/21 assertions 通过,证据为 `.lime/qc/project-gates/standalone-shell-01-20260731104641-30858/shell-01-electron-smoke/summary.json`。
|
||||
|
||||
产品证据:success 场景经真实 Electron、preload/IPC、`app_server_handle_json_lines`、App Server、RuntimeCore、read model 与 GUI 观察 `error(willRetry=true) -> turn/completed(completed)`,同一 identity、零生产 mock fallback,证据为 `.lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-typed-error-retry-success-gateb-02-summary.json`。failure 场景经同一链观察 `error(true)* -> error(false) -> turn/completed(failed)`,pending read model、GUI failed 与最终 read-model/后台 terminal 一致,证据为 `.lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-typed-error-retry-failure-gateb-03-summary.json`。两者 `ok=true`、proof level 均为 `Gate B controlled fixture`;聚合回归证据分别为 `claw-chat-current-fixture-typed-error-retry-success-regression-summary.json` 与 `claw-chat-current-fixture-typed-error-retry-failure-regression-summary.json`。
|
||||
|
||||
治理分类:v2 typed `error` protocol/projector、durable runtime error recovery、strict Renderer signal 与权威 Turn terminal ownership 为 `current`;无 `compat`;raw error wrapper、provider trace 对 current 主链证据的错误替代和由 error 抢占 Turn terminal 的第二状态机为 `dead / forbidden-to-restore`;其余 V2-05 planned notification、host capability 与全面 recovery 仍为开放项。
|
||||
|
||||
架构确认:本切片没有改变 public owner、唯一产品链或依赖方向,只补齐既有 notification/projector/read-model/Renderer 边界的 typed contract 与恢复语义;无需改写 `internal/aiprompts/architecture.md`。责任开发者确认:root,2026-07-31。
|
||||
|
||||
下一刀:回到 `EVENT-PROJECTIONS.md` 审计下一项具备真实 producer/consumer 的 planned notification、host capability 或 recovery;`guardianWarning` 在 Guardian runtime producer 落地前保持 planned。v2 总体完成度保守估算约 93%。
|
||||
|
||||
### 2026-07-31:V2-05 `turn/plan/updated` canonical checklist 与恢复
|
||||
|
||||
状态:该垂直切片 `completed`;V2-05 与 v2 总体仍为 `in-progress`,不得标记 release-ready。
|
||||
|
||||
本轮实现:
|
||||
|
||||
- App Server 从 canonical `update_plan` `ToolOutput.structured_content` 派生 typed `turn.plan.updated`;Renderer 严格解析该通知并把实时快照投影到唯一 run-control checklist owner。
|
||||
- canonical cold read 从成功的 `update_plan` 工具项恢复最新有效计划,兼容 App Server 真实 wire 的 `[ { name: "plan", value: "[...]" } ]` 参数形状;空数组可以清空旧 checklist,失败或非法快照不会覆盖上一份有效计划。
|
||||
- read model 保留 canonical `update_plan` 工具项以支持恢复,但不生成 `ThreadItem.plan`,不从 Message/tool card 合成 checklist,也不展示 Plan UI、decision panel 或 `update_plan` 工具行。
|
||||
|
||||
验证:`appServerCanonicalThreadProjection.test.ts` 10/10;`npm run test:contracts` 通过,app-server-client contract 为 292 checks;`npm run typecheck`、`npm run smoke:agent-runtime-current-fixture` 与 `npm run verify:gui-smoke` 均通过。Gate B 证据为 `.lime/qc/gui-evidence/claw-chat-current-fixture/claw-chat-current-fixture-summary.json`:`scenario=turn-plan-update`、`proofLevel=Gate B controlled fixture`、`methodCount=1`、通知位于事件序列 `planIndex=16` 且 `turn/completed` 位于 `terminalIndex=27`;实时与 reload 后均可见 2 项 checklist,canonical read model `updatePlanToolCount=1`、`planItemCount=0`,Plan block/decision/update_plan tool row 均为 0,Electron IPC、App Server、read model 与 GUI identity 一致,invoke/page/console error 为 0。
|
||||
|
||||
治理分类:`turn/plan/updated` typed producer、strict Renderer parser、canonical `update_plan` cold recovery 与 run-control checklist 为 `current`;无 `compat`;`update_plan` 场景下的 Plan ThreadItem、Message/tool card 合成、Plan UI/decision panel/tool card 和 renderer mock fallback 均为 `dead / forbidden-to-restore`,独立 Plan mode 的现役 UI 不在本切片范围内。本切片没有改变 public owner、唯一产品链或依赖方向,无需改写 `internal/aiprompts/architecture.md`。
|
||||
|
||||
下一刀:回到 `EVENT-PROJECTIONS.md` 审计下一项具备真实 producer/consumer 的 planned notification、host capability 或 recovery;`guardianWarning` 在 Guardian runtime producer 落地前保持 planned。v2 总体完成度保守估算约 94%。
|
||||
|
||||
### 2026-07-31:V2-05 MCP OAuth typed completion notification
|
||||
|
||||
状态:该垂直切片 `completed`;V2-05 与 v2 总体仍为 `in-progress`,不得标记 release-ready。
|
||||
|
||||
本轮实现:
|
||||
|
||||
- Rust MCP OAuth 返回 `McpOAuthLoginHandle` completion future;App Server 等待 callback 成功或失败后发布 `mcpServer/oauthLogin/completed`,payload 固定为必填 `name`、必填可空 `threadId`、`success` 与可选 `error`。
|
||||
- protocol v2、schema、generated TypeScript 与 app-server-client strict decoder 使用同一 typed contract。Renderer 在任何异步 Desktop listener 注册前同步订阅 App Server event bus,避免快速 callback 被排空,并在完成后自动刷新 `mcpServerStatus/list` 与 `mcpTool/list`。
|
||||
- 生产路径已删除 `mcp:oauth_completed`;contract guard 禁止该字符串回流 `electron`、`lime-rs`、`packages` 与 `src`。没有新增 compat wrapper、第二 OAuth 状态 owner或生产 mock fallback。
|
||||
|
||||
验证:`lime-mcp` OAuth 22 passed、app-server-protocol v2 59 passed、App Server MCP processor 4 passed、app-server-client 101 passed、`src/hooks/useMcp.test.tsx` 9 passed;`npm run test:contracts` 通过,包含 847 schema definitions、839 generated protocol types、0 generation failures 与 292 app-server-client checks。
|
||||
|
||||
产品证据:`npm run smoke:mcp-oauth-notification-electron-fixture` 通过,proof level 为 `Gate B`。真实 Electron/preload/IPC、`app_server_handle_json_lines`、App Server OAuth login/event drain、本地 OAuth provider callback 与 Renderer GUI 链全部命中;授权前后状态和 completion toast 均可见,`mcpServer/oauth/login`、`mcpServerStatus/list`、`mcpTool/list` 自动刷新成立,`openExternalUrlHitCount=1`、`electronIpcHitCount=8`、`mockFallbackHitCount=0`、`failedInvokeCount=0`,console/page/invoke errors 均为 0。证据为 `.lime/qc/mcp-oauth-notification/mcp-oauth-notification-fixture-summary.json` 与同目录 PNG。
|
||||
|
||||
治理分类:typed protocol/schema、App Server completion producer、strict client decoder、Renderer typed event bus 与自动刷新为 `current`;无 `compat`;`mcp:oauth_completed` 为 `dead / deleted / forbidden-to-restore`。本切片没有改变 public owner、唯一产品链或依赖方向,无需改写 `internal/aiprompts/architecture.md`。责任开发者确认:root,2026-07-31。
|
||||
|
||||
下一刀:回到 `EVENT-PROJECTIONS.md` 继续审计具备真实 producer/consumer 的 planned notification、host capability 或 recovery;`guardianWarning` 在 Guardian runtime producer 落地前保持 planned。v2 总体完成度保守估算约 95%,仍非 release-ready。
|
||||
|
||||
Generated
+31
-30
@@ -21,7 +21,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-protocol"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -30,7 +30,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-runtime"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"anyhow",
|
||||
@@ -181,7 +181,7 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "app-server"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"agent-runtime",
|
||||
@@ -209,6 +209,7 @@ dependencies = [
|
||||
"lime-services",
|
||||
"lime-skills",
|
||||
"model-provider",
|
||||
"notify",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"opentelemetry_sdk",
|
||||
@@ -241,7 +242,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "app-server-client"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"app-server-protocol",
|
||||
"app-server-transport",
|
||||
@@ -252,7 +253,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "app-server-daemon"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"app-server-protocol",
|
||||
"app-server-transport",
|
||||
@@ -264,7 +265,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "app-server-protocol"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"schemars",
|
||||
@@ -274,7 +275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "app-server-test-client"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"app-server-client",
|
||||
@@ -285,7 +286,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "app-server-transport"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"app-server-protocol",
|
||||
"axum",
|
||||
@@ -1355,7 +1356,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "document-preview"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"tempfile",
|
||||
@@ -2618,7 +2619,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-agent"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"agent-runtime",
|
||||
@@ -2651,7 +2652,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-browser-runtime"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
@@ -2668,7 +2669,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-cli"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"lime-core",
|
||||
@@ -2680,7 +2681,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-config"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"lime-core",
|
||||
@@ -2696,7 +2697,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-core"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"async-trait",
|
||||
@@ -2752,7 +2753,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-gateway"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"async-trait",
|
||||
@@ -2780,7 +2781,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-infra"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -2802,7 +2803,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-knowledge"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
@@ -2815,7 +2816,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-mcp"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -2841,7 +2842,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-media-runtime"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
@@ -2860,7 +2861,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-processor"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"lime-core",
|
||||
@@ -2900,7 +2901,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-server"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"async-stream",
|
||||
@@ -2943,7 +2944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-server-utils"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures",
|
||||
@@ -2958,7 +2959,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-services"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3001,7 +3002,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-skills"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dirs 5.0.1",
|
||||
@@ -3022,7 +3023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lime-websocket"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -3231,7 +3232,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "model-provider"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"anyhow",
|
||||
@@ -3901,7 +3902,7 @@ checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec"
|
||||
|
||||
[[package]]
|
||||
name = "patch-apply"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
@@ -4660,7 +4661,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "runtime-core"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"app-server-protocol",
|
||||
@@ -5408,7 +5409,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thread-store"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"chrono",
|
||||
@@ -5686,7 +5687,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tool-runtime"
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
dependencies = [
|
||||
"agent-protocol",
|
||||
"app-server-protocol",
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ exclude = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "1.117.0"
|
||||
version = "1.118.0"
|
||||
edition = "2021"
|
||||
authors = ["coso"]
|
||||
repository = "https://github.com/aiclientproxy/lime"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -218,6 +218,10 @@
|
||||
"kind": "request",
|
||||
"method": "discordChannel/probe"
|
||||
},
|
||||
{
|
||||
"kind": "notification",
|
||||
"method": "error"
|
||||
},
|
||||
{
|
||||
"kind": "request",
|
||||
"method": "evidence/export"
|
||||
@@ -522,6 +526,10 @@
|
||||
"kind": "request",
|
||||
"method": "mcpServer/oauth/login"
|
||||
},
|
||||
{
|
||||
"kind": "notification",
|
||||
"method": "mcpServer/oauthLogin/completed"
|
||||
},
|
||||
{
|
||||
"kind": "request",
|
||||
"method": "mcpServer/start"
|
||||
@@ -994,6 +1002,10 @@
|
||||
"kind": "request",
|
||||
"method": "skillRepository/save"
|
||||
},
|
||||
{
|
||||
"kind": "notification",
|
||||
"method": "skills/changed"
|
||||
},
|
||||
{
|
||||
"kind": "request",
|
||||
"method": "soulStylePack/install"
|
||||
@@ -1186,6 +1198,10 @@
|
||||
"kind": "request",
|
||||
"method": "turn/interrupt"
|
||||
},
|
||||
{
|
||||
"kind": "notification",
|
||||
"method": "turn/plan/updated"
|
||||
},
|
||||
{
|
||||
"kind": "request",
|
||||
"method": "turn/start"
|
||||
@@ -2190,6 +2206,7 @@
|
||||
"ArtifactWriteSidecar",
|
||||
"ClientRequest",
|
||||
"ClientResponse",
|
||||
"CodexErrorInfo",
|
||||
"CollabAgentState",
|
||||
"CollabAgentStatus",
|
||||
"CollabAgentTool",
|
||||
@@ -2212,6 +2229,7 @@
|
||||
"DynamicToolNamespaceSpec",
|
||||
"DynamicToolNamespaceTool",
|
||||
"DynamicToolSpec",
|
||||
"ErrorNotification",
|
||||
"FileChangeApprovalDecision",
|
||||
"FileChangePatchUpdatedNotification",
|
||||
"FileChangeRequestApprovalParams",
|
||||
@@ -2232,6 +2250,7 @@
|
||||
"McpServerElicitationAction",
|
||||
"McpServerElicitationRequestParams",
|
||||
"McpServerElicitationRequestResponse",
|
||||
"McpServerOauthLoginCompletedNotification",
|
||||
"McpToolCallAppContext",
|
||||
"McpToolCallError",
|
||||
"McpToolCallProgressNotification",
|
||||
@@ -2254,6 +2273,7 @@
|
||||
"ModelUpgradeInfo",
|
||||
"ModelVerification",
|
||||
"ModelVerificationNotification",
|
||||
"NonSteerableTurnKind",
|
||||
"PatchApplyStatus",
|
||||
"PatchChangeKind",
|
||||
"PermissionGrantScope",
|
||||
@@ -2268,6 +2288,7 @@
|
||||
"ServerNotification",
|
||||
"ServerRequest",
|
||||
"ServerRequestResolvedNotification",
|
||||
"SkillsChangedNotification",
|
||||
"SleepItem",
|
||||
"SortDirection",
|
||||
"SubAgentActivityKind",
|
||||
@@ -2379,6 +2400,9 @@
|
||||
"TurnInterruptParams",
|
||||
"TurnInterruptResponse",
|
||||
"TurnItemsView",
|
||||
"TurnPlanStep",
|
||||
"TurnPlanStepStatus",
|
||||
"TurnPlanUpdatedNotification",
|
||||
"TurnStartParams",
|
||||
"TurnStartResponse",
|
||||
"TurnStartedNotification",
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"$defs": {
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
],
|
||||
"title": "CodexErrorInfo"
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"$defs": {
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"description": "Reports a transient retry or terminal error for the current turn.",
|
||||
"properties": {
|
||||
"error": {
|
||||
"$ref": "#/$defs/TurnError"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
},
|
||||
"willRetry": {
|
||||
"description": "True only when App Server has already decided to retry this turn.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error",
|
||||
"threadId",
|
||||
"turnId",
|
||||
"willRetry"
|
||||
],
|
||||
"title": "ErrorNotification",
|
||||
"type": "object"
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"threadId": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"success",
|
||||
"threadId"
|
||||
],
|
||||
"title": "McpServerOauthLoginCompletedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"title": "NonSteerableTurnKind",
|
||||
"type": "string"
|
||||
}
|
||||
@@ -44,6 +44,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -363,6 +496,32 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ErrorNotification": {
|
||||
"additionalProperties": false,
|
||||
"description": "Reports a transient retry or terminal error for the current turn.",
|
||||
"properties": {
|
||||
"error": {
|
||||
"$ref": "#/$defs/TurnError"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
},
|
||||
"willRetry": {
|
||||
"description": "True only when App Server has already decided to retry this turn.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error",
|
||||
"threadId",
|
||||
"turnId",
|
||||
"willRetry"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileChangePatchUpdatedNotification": {
|
||||
"properties": {
|
||||
"changes": {
|
||||
@@ -534,6 +693,35 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpServerOauthLoginCompletedNotification": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"threadId": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"success",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"McpToolCallAppContext": {
|
||||
"properties": {
|
||||
"actionName": {
|
||||
@@ -792,6 +980,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -978,6 +1173,11 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SkillsChangedNotification": {
|
||||
"additionalProperties": false,
|
||||
"description": "Signals that the process-wide Skill catalog must be read again.",
|
||||
"type": "object"
|
||||
},
|
||||
"SleepItem": {
|
||||
"properties": {
|
||||
"durationMs": {
|
||||
@@ -2249,6 +2449,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -2256,7 +2457,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2274,6 +2484,61 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TurnPlanStep": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "#/$defs/TurnPlanStepStatus"
|
||||
},
|
||||
"step": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"step"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnPlanStepStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
"inProgress",
|
||||
"pending"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"TurnPlanUpdatedNotification": {
|
||||
"additionalProperties": false,
|
||||
"description": "Reports the latest turn-scoped execution checklist.",
|
||||
"properties": {
|
||||
"explanation": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"plan": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/TurnPlanStep"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plan",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnStartedNotification": {
|
||||
"description": "Notification payloads are kept beside the request contract so the v2\nprojection can use the same Thread/Turn identity without a second DTO.",
|
||||
"properties": {
|
||||
@@ -2499,6 +2764,54 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"const": "error",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/$defs/ErrorNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"const": "skills/changed",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/$defs/SkillsChangedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"const": "mcpServer/oauthLogin/completed",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/$defs/McpServerOauthLoginCompletedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -2643,6 +2956,22 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"const": "turn/plan/updated",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/$defs/TurnPlanUpdatedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"description": "Signals that the process-wide Skill catalog must be read again.",
|
||||
"title": "SkillsChangedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -441,6 +574,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1261,6 +1401,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1268,7 +1409,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -465,6 +598,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1426,6 +1566,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1433,7 +1574,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -441,6 +574,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1402,6 +1542,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1409,7 +1550,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
+151
-1
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -441,6 +574,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1402,6 +1542,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1409,7 +1550,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -441,6 +574,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1402,6 +1542,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1409,7 +1550,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -465,6 +598,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1426,6 +1566,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1433,7 +1574,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -441,6 +574,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1417,6 +1557,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1424,7 +1565,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -441,6 +574,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1402,6 +1542,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1409,7 +1550,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -465,6 +598,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1426,6 +1566,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1433,7 +1574,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -441,6 +574,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1402,6 +1542,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1409,7 +1550,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -418,6 +551,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1164,6 +1304,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1171,7 +1312,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -441,6 +574,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1402,6 +1542,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1409,7 +1550,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -418,6 +551,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1107,6 +1247,7 @@
|
||||
]
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1114,7 +1255,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -418,6 +551,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1164,6 +1304,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1171,7 +1312,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,148 @@
|
||||
{
|
||||
"$defs": {
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -7,7 +150,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$defs": {
|
||||
"TurnPlanStepStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
"inProgress",
|
||||
"pending"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "#/$defs/TurnPlanStepStatus"
|
||||
},
|
||||
"step": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"step"
|
||||
],
|
||||
"title": "TurnPlanStep",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"enum": [
|
||||
"completed",
|
||||
"inProgress",
|
||||
"pending"
|
||||
],
|
||||
"title": "TurnPlanStepStatus",
|
||||
"type": "string"
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$defs": {
|
||||
"TurnPlanStep": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "#/$defs/TurnPlanStepStatus"
|
||||
},
|
||||
"step": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"step"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TurnPlanStepStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
"inProgress",
|
||||
"pending"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"description": "Reports the latest turn-scoped execution checklist.",
|
||||
"properties": {
|
||||
"explanation": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"plan": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/TurnPlanStep"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
},
|
||||
"turnId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plan",
|
||||
"threadId",
|
||||
"turnId"
|
||||
],
|
||||
"title": "TurnPlanUpdatedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -418,6 +551,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1164,6 +1304,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1171,7 +1312,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -418,6 +551,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1164,6 +1304,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1171,7 +1312,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,139 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodexErrorInfo": {
|
||||
"description": "Codex-compatible error classification exposed on the v2 wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"enum": [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"httpConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"httpConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamConnectionFailed": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamConnectionFailed"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseStreamDisconnected": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseStreamDisconnected"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"responseTooManyFailedAttempts": {
|
||||
"properties": {
|
||||
"httpStatusCode": {
|
||||
"format": "uint16",
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"responseTooManyFailedAttempts"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"activeTurnNotSteerable": {
|
||||
"properties": {
|
||||
"turnKind": {
|
||||
"$ref": "#/$defs/NonSteerableTurnKind"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"turnKind"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"activeTurnNotSteerable"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CollabAgentState": {
|
||||
"properties": {
|
||||
"message": {
|
||||
@@ -418,6 +551,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NonSteerableTurnKind": {
|
||||
"enum": [
|
||||
"compact",
|
||||
"review"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PatchApplyStatus": {
|
||||
"enum": [
|
||||
"completed",
|
||||
@@ -1164,6 +1304,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"TurnError": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalDetails": {
|
||||
"type": [
|
||||
@@ -1171,7 +1312,16 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"errorInfo": true,
|
||||
"codexErrorInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CodexErrorInfo"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -262,12 +262,56 @@ pub struct Turn {
|
||||
pub duration_ms: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum NonSteerableTurnKind {
|
||||
Review,
|
||||
Compact,
|
||||
}
|
||||
|
||||
/// Codex-compatible error classification exposed on the v2 wire.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum CodexErrorInfo {
|
||||
ContextWindowExceeded,
|
||||
SessionBudgetExceeded,
|
||||
UsageLimitExceeded,
|
||||
ServerOverloaded,
|
||||
CyberPolicy,
|
||||
HttpConnectionFailed {
|
||||
#[serde(rename = "httpStatusCode")]
|
||||
http_status_code: Option<u16>,
|
||||
},
|
||||
ResponseStreamConnectionFailed {
|
||||
#[serde(rename = "httpStatusCode")]
|
||||
http_status_code: Option<u16>,
|
||||
},
|
||||
InternalServerError,
|
||||
Unauthorized,
|
||||
BadRequest,
|
||||
ThreadRollbackFailed,
|
||||
SandboxError,
|
||||
ResponseStreamDisconnected {
|
||||
#[serde(rename = "httpStatusCode")]
|
||||
http_status_code: Option<u16>,
|
||||
},
|
||||
ResponseTooManyFailedAttempts {
|
||||
#[serde(rename = "httpStatusCode")]
|
||||
http_status_code: Option<u16>,
|
||||
},
|
||||
ActiveTurnNotSteerable {
|
||||
#[serde(rename = "turnKind")]
|
||||
turn_kind: NonSteerableTurnKind,
|
||||
},
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct TurnError {
|
||||
pub message: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error_info: Option<Value>,
|
||||
pub codex_error_info: Option<CodexErrorInfo>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub additional_details: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use super::{
|
||||
AgentMessageDeltaNotification, ArtifactWriteParams, ArtifactWriteResponse,
|
||||
CommandExecutionOutputDeltaNotification, CommandExecutionRequestApprovalParams,
|
||||
ConfigWarningNotification, CurrentTimeReadParams, DynamicToolCallParams,
|
||||
ConfigWarningNotification, CurrentTimeReadParams, DynamicToolCallParams, ErrorNotification,
|
||||
FileChangePatchUpdatedNotification, FileChangeRequestApprovalParams, ItemCompletedNotification,
|
||||
ItemStartedNotification, McpServerElicitationRequestParams, McpToolCallProgressNotification,
|
||||
MediaReadParams, MediaReadResponse, Method, ModelListParams, ModelListUpdatedNotification,
|
||||
ItemStartedNotification, McpServerElicitationRequestParams,
|
||||
McpServerOauthLoginCompletedNotification, McpToolCallProgressNotification, MediaReadParams,
|
||||
MediaReadResponse, Method, ModelListParams, ModelListUpdatedNotification,
|
||||
ModelReroutedNotification, ModelSafetyBufferingUpdatedNotification,
|
||||
ModelVerificationNotification, PermissionsRequestApprovalParams, PlanDeltaNotification,
|
||||
ReasoningSummaryPartAddedNotification, ReasoningSummaryTextDeltaNotification,
|
||||
ReasoningTextDeltaNotification, ServerRequestResolvedNotification,
|
||||
ReasoningTextDeltaNotification, ServerRequestResolvedNotification, SkillsChangedNotification,
|
||||
ThreadApproveGuardianDeniedActionParams, ThreadApproveGuardianDeniedActionResponse,
|
||||
ThreadArchiveParams, ThreadArchiveResponse, ThreadArchivedNotification,
|
||||
ThreadBackgroundTerminalsCleanParams, ThreadBackgroundTerminalsCleanResponse,
|
||||
@@ -33,19 +34,21 @@ use super::{
|
||||
ThreadTokenUsageUpdatedNotification, ThreadTurnsListParams, ThreadTurnsListResponse,
|
||||
ThreadUnarchiveParams, ThreadUnarchiveResponse, ThreadUnarchivedNotification,
|
||||
ThreadUnsubscribeParams, ThreadUnsubscribeResponse, ToolRequestUserInputParams,
|
||||
TurnCompletedNotification, TurnInterruptParams, TurnInterruptResponse, TurnStartParams,
|
||||
TurnStartResponse, TurnStartedNotification, TurnSteerParams, TurnSteerResponse,
|
||||
WarningNotification, METHOD_COMMAND_EXECUTION_OUTPUT_DELTA, METHOD_CONFIG_WARNING,
|
||||
METHOD_CURRENT_TIME_READ, METHOD_FILE_CHANGE_PATCH_UPDATED,
|
||||
METHOD_ITEM_COMMAND_EXECUTION_REQUEST_APPROVAL, METHOD_ITEM_FILE_CHANGE_REQUEST_APPROVAL,
|
||||
METHOD_ITEM_PERMISSIONS_REQUEST_APPROVAL, METHOD_ITEM_TOOL_CALL,
|
||||
METHOD_ITEM_TOOL_REQUEST_USER_INPUT, METHOD_MCP_SERVER_ELICITATION_REQUEST,
|
||||
TurnCompletedNotification, TurnInterruptParams, TurnInterruptResponse,
|
||||
TurnPlanUpdatedNotification, TurnStartParams, TurnStartResponse, TurnStartedNotification,
|
||||
TurnSteerParams, TurnSteerResponse, WarningNotification, METHOD_COMMAND_EXECUTION_OUTPUT_DELTA,
|
||||
METHOD_CONFIG_WARNING, METHOD_CURRENT_TIME_READ, METHOD_ERROR,
|
||||
METHOD_FILE_CHANGE_PATCH_UPDATED, METHOD_ITEM_COMMAND_EXECUTION_REQUEST_APPROVAL,
|
||||
METHOD_ITEM_FILE_CHANGE_REQUEST_APPROVAL, METHOD_ITEM_PERMISSIONS_REQUEST_APPROVAL,
|
||||
METHOD_ITEM_TOOL_CALL, METHOD_ITEM_TOOL_REQUEST_USER_INPUT,
|
||||
METHOD_MCP_SERVER_ELICITATION_REQUEST, METHOD_MCP_SERVER_OAUTH_LOGIN_COMPLETED,
|
||||
METHOD_MCP_TOOL_CALL_PROGRESS, METHOD_MODEL_LIST_UPDATED, METHOD_MODEL_REROUTED,
|
||||
METHOD_MODEL_SAFETY_BUFFERING_UPDATED, METHOD_MODEL_VERIFICATION, METHOD_PLAN_DELTA,
|
||||
METHOD_REASONING_SUMMARY_PART_ADDED, METHOD_REASONING_SUMMARY_TEXT_DELTA,
|
||||
METHOD_REASONING_TEXT_DELTA, METHOD_SERVER_REQUEST_RESOLVED, METHOD_THREAD_CLOSED,
|
||||
METHOD_THREAD_GOAL_CLEARED, METHOD_THREAD_GOAL_UPDATED, METHOD_THREAD_NAME_UPDATED,
|
||||
METHOD_THREAD_STATUS_CHANGED, METHOD_THREAD_TOKEN_USAGE_UPDATED, METHOD_WARNING,
|
||||
METHOD_REASONING_TEXT_DELTA, METHOD_SERVER_REQUEST_RESOLVED, METHOD_SKILLS_CHANGED,
|
||||
METHOD_THREAD_CLOSED, METHOD_THREAD_GOAL_CLEARED, METHOD_THREAD_GOAL_UPDATED,
|
||||
METHOD_THREAD_NAME_UPDATED, METHOD_THREAD_STATUS_CHANGED, METHOD_THREAD_TOKEN_USAGE_UPDATED,
|
||||
METHOD_TURN_PLAN_UPDATED, METHOD_WARNING,
|
||||
};
|
||||
use crate::{JsonRpcNotification, JsonRpcRequest, RequestId};
|
||||
use schemars::JsonSchema;
|
||||
@@ -645,6 +648,12 @@ pub enum ServerNotification {
|
||||
ConfigWarning(ConfigWarningNotification),
|
||||
#[serde(rename = "warning")]
|
||||
Warning(WarningNotification),
|
||||
#[serde(rename = "error")]
|
||||
Error(ErrorNotification),
|
||||
#[serde(rename = "skills/changed")]
|
||||
SkillsChanged(SkillsChangedNotification),
|
||||
#[serde(rename = "mcpServer/oauthLogin/completed")]
|
||||
McpServerOauthLoginCompleted(McpServerOauthLoginCompletedNotification),
|
||||
#[serde(rename = "thread/started")]
|
||||
ThreadStarted(ThreadStartedNotification),
|
||||
#[serde(rename = "thread/archived")]
|
||||
@@ -663,6 +672,8 @@ pub enum ServerNotification {
|
||||
TurnStarted(TurnStartedNotification),
|
||||
#[serde(rename = "turn/completed")]
|
||||
TurnCompleted(TurnCompletedNotification),
|
||||
#[serde(rename = "turn/plan/updated")]
|
||||
TurnPlanUpdated(TurnPlanUpdatedNotification),
|
||||
#[serde(rename = "item/started")]
|
||||
ItemStarted(ItemStartedNotification),
|
||||
#[serde(rename = "item/completed")]
|
||||
@@ -708,6 +719,9 @@ impl ServerNotification {
|
||||
match self {
|
||||
Self::ConfigWarning(_) => METHOD_CONFIG_WARNING,
|
||||
Self::Warning(_) => METHOD_WARNING,
|
||||
Self::Error(_) => METHOD_ERROR,
|
||||
Self::SkillsChanged(_) => METHOD_SKILLS_CHANGED,
|
||||
Self::McpServerOauthLoginCompleted(_) => METHOD_MCP_SERVER_OAUTH_LOGIN_COMPLETED,
|
||||
Self::ThreadStarted(_) => "thread/started",
|
||||
Self::ThreadArchived(_) => "thread/archived",
|
||||
Self::ThreadDeleted(_) => "thread/deleted",
|
||||
@@ -717,6 +731,7 @@ impl ServerNotification {
|
||||
Self::ThreadStatusChanged(_) => METHOD_THREAD_STATUS_CHANGED,
|
||||
Self::TurnStarted(_) => "turn/started",
|
||||
Self::TurnCompleted(_) => "turn/completed",
|
||||
Self::TurnPlanUpdated(_) => METHOD_TURN_PLAN_UPDATED,
|
||||
Self::ItemStarted(_) => "item/started",
|
||||
Self::ItemCompleted(_) => "item/completed",
|
||||
Self::AgentMessageDelta(_) => "item/agentMessage/delta",
|
||||
@@ -743,7 +758,7 @@ impl ServerNotification {
|
||||
impl TryFrom<JsonRpcNotification> for ServerNotification {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(notification: JsonRpcNotification) -> Result<Self, Self::Error> {
|
||||
fn try_from(notification: JsonRpcNotification) -> Result<Self, String> {
|
||||
let params = notification.params.unwrap_or_else(|| serde_json::json!({}));
|
||||
match notification.method.as_str() {
|
||||
METHOD_CONFIG_WARNING => serde_json::from_value(params)
|
||||
@@ -752,6 +767,15 @@ impl TryFrom<JsonRpcNotification> for ServerNotification {
|
||||
METHOD_WARNING => serde_json::from_value(params)
|
||||
.map(Self::Warning)
|
||||
.map_err(|error| error.to_string()),
|
||||
METHOD_ERROR => serde_json::from_value(params)
|
||||
.map(Self::Error)
|
||||
.map_err(|error| error.to_string()),
|
||||
METHOD_SKILLS_CHANGED => serde_json::from_value(params)
|
||||
.map(Self::SkillsChanged)
|
||||
.map_err(|error| error.to_string()),
|
||||
METHOD_MCP_SERVER_OAUTH_LOGIN_COMPLETED => serde_json::from_value(params)
|
||||
.map(Self::McpServerOauthLoginCompleted)
|
||||
.map_err(|error| error.to_string()),
|
||||
"thread/started" => serde_json::from_value(params)
|
||||
.map(Self::ThreadStarted)
|
||||
.map_err(|error| error.to_string()),
|
||||
@@ -779,6 +803,9 @@ impl TryFrom<JsonRpcNotification> for ServerNotification {
|
||||
"turn/completed" => serde_json::from_value(params)
|
||||
.map(Self::TurnCompleted)
|
||||
.map_err(|error| error.to_string()),
|
||||
METHOD_TURN_PLAN_UPDATED => serde_json::from_value(params)
|
||||
.map(Self::TurnPlanUpdated)
|
||||
.map_err(|error| error.to_string()),
|
||||
"item/started" => serde_json::from_value(params)
|
||||
.map(Self::ItemStarted)
|
||||
.map_err(|error| error.to_string()),
|
||||
@@ -848,6 +875,13 @@ impl From<ServerNotification> for JsonRpcNotification {
|
||||
jsonrpc_notification(METHOD_CONFIG_WARNING, params)
|
||||
}
|
||||
ServerNotification::Warning(params) => jsonrpc_notification(METHOD_WARNING, params),
|
||||
ServerNotification::Error(params) => jsonrpc_notification(METHOD_ERROR, params),
|
||||
ServerNotification::SkillsChanged(params) => {
|
||||
jsonrpc_notification(METHOD_SKILLS_CHANGED, params)
|
||||
}
|
||||
ServerNotification::McpServerOauthLoginCompleted(params) => {
|
||||
jsonrpc_notification(METHOD_MCP_SERVER_OAUTH_LOGIN_COMPLETED, params)
|
||||
}
|
||||
ServerNotification::ThreadStarted(params) => {
|
||||
jsonrpc_notification("thread/started", params)
|
||||
}
|
||||
@@ -873,6 +907,9 @@ impl From<ServerNotification> for JsonRpcNotification {
|
||||
ServerNotification::TurnCompleted(params) => {
|
||||
jsonrpc_notification("turn/completed", params)
|
||||
}
|
||||
ServerNotification::TurnPlanUpdated(params) => {
|
||||
jsonrpc_notification(METHOD_TURN_PLAN_UPDATED, params)
|
||||
}
|
||||
ServerNotification::ItemStarted(params) => jsonrpc_notification("item/started", params),
|
||||
ServerNotification::ItemCompleted(params) => {
|
||||
jsonrpc_notification("item/completed", params)
|
||||
|
||||
@@ -28,6 +28,21 @@ pub struct McpToolCallProgressNotification {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct McpServerOauthLoginCompletedNotification {
|
||||
pub name: String,
|
||||
#[schemars(schema_with = "nullable_string_schema")]
|
||||
pub thread_id: Option<String>,
|
||||
pub success: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
fn nullable_string_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
generator.subschema_for::<Option<String>>()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct McpServerElicitationRequestParams {
|
||||
@@ -238,4 +253,47 @@ mod tests {
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_login_completed_matches_codex_wire_and_fails_closed() {
|
||||
let success = McpServerOauthLoginCompletedNotification {
|
||||
name: "remote-docs".to_string(),
|
||||
thread_id: None,
|
||||
success: true,
|
||||
error: None,
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(success).expect("serialize OAuth completion"),
|
||||
json!({
|
||||
"name": "remote-docs",
|
||||
"threadId": null,
|
||||
"success": true
|
||||
})
|
||||
);
|
||||
|
||||
assert!(
|
||||
serde_json::from_value::<McpServerOauthLoginCompletedNotification>(json!({
|
||||
"name": "remote-docs",
|
||||
"threadId": null,
|
||||
"success": false,
|
||||
"error": "scope rejected",
|
||||
"serverName": "legacy-name"
|
||||
}))
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let schema = serde_json::to_value(schemars::schema_for!(
|
||||
McpServerOauthLoginCompletedNotification
|
||||
))
|
||||
.expect("serialize OAuth completion schema");
|
||||
let required = schema["required"]
|
||||
.as_array()
|
||||
.expect("OAuth completion schema required fields");
|
||||
assert!(required.iter().any(|field| field == "threadId"));
|
||||
let thread_id_types = schema["properties"]["threadId"]["type"]
|
||||
.as_array()
|
||||
.expect("OAuth completion threadId must be nullable");
|
||||
assert!(thread_id_types.iter().any(|value| value == "null"));
|
||||
assert!(thread_id_types.iter().any(|value| value == "string"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ pub const METHOD_THREAD_NAME_UPDATED: &str = "thread/name/updated";
|
||||
pub const METHOD_THREAD_STATUS_CHANGED: &str = "thread/status/changed";
|
||||
pub const METHOD_TURN_STARTED: &str = "turn/started";
|
||||
pub const METHOD_TURN_COMPLETED: &str = "turn/completed";
|
||||
pub const METHOD_TURN_PLAN_UPDATED: &str = "turn/plan/updated";
|
||||
pub const METHOD_ITEM_STARTED: &str = "item/started";
|
||||
pub const METHOD_ITEM_COMPLETED: &str = "item/completed";
|
||||
pub const METHOD_AGENT_MESSAGE_DELTA: &str = "item/agentMessage/delta";
|
||||
@@ -68,6 +69,9 @@ pub const METHOD_THREAD_GOAL_CLEARED: &str = "thread/goal/cleared";
|
||||
pub const METHOD_SERVER_REQUEST_RESOLVED: &str = "serverRequest/resolved";
|
||||
pub const METHOD_CONFIG_WARNING: &str = "configWarning";
|
||||
pub const METHOD_WARNING: &str = "warning";
|
||||
pub const METHOD_ERROR: &str = "error";
|
||||
pub const METHOD_SKILLS_CHANGED: &str = "skills/changed";
|
||||
pub const METHOD_MCP_SERVER_OAUTH_LOGIN_COMPLETED: &str = "mcpServer/oauthLogin/completed";
|
||||
pub const METHOD_CURRENT_TIME_READ: &str = "currentTime/read";
|
||||
pub const METHOD_MCP_SERVER_ELICITATION_REQUEST: &str = "mcpServer/elicitation/request";
|
||||
pub const METHOD_ITEM_COMMAND_EXECUTION_REQUEST_APPROVAL: &str =
|
||||
@@ -287,6 +291,9 @@ pub const METHODS: &[&str] = &[
|
||||
pub const NOTIFICATION_METHODS: &[&str] = &[
|
||||
METHOD_CONFIG_WARNING,
|
||||
METHOD_WARNING,
|
||||
METHOD_ERROR,
|
||||
METHOD_SKILLS_CHANGED,
|
||||
METHOD_MCP_SERVER_OAUTH_LOGIN_COMPLETED,
|
||||
METHOD_THREAD_STARTED,
|
||||
METHOD_THREAD_ARCHIVED,
|
||||
METHOD_THREAD_DELETED,
|
||||
@@ -296,6 +303,7 @@ pub const NOTIFICATION_METHODS: &[&str] = &[
|
||||
METHOD_THREAD_STATUS_CHANGED,
|
||||
METHOD_TURN_STARTED,
|
||||
METHOD_TURN_COMPLETED,
|
||||
METHOD_TURN_PLAN_UPDATED,
|
||||
METHOD_ITEM_STARTED,
|
||||
METHOD_ITEM_COMPLETED,
|
||||
METHOD_AGENT_MESSAGE_DELTA,
|
||||
|
||||
@@ -1,7 +1,50 @@
|
||||
use super::TurnError;
|
||||
use crate::RequestId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Signals that the process-wide Skill catalog must be read again.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SkillsChangedNotification {}
|
||||
|
||||
/// Reports a transient retry or terminal error for the current turn.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct ErrorNotification {
|
||||
pub error: TurnError,
|
||||
/// True only when App Server has already decided to retry this turn.
|
||||
pub will_retry: bool,
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
}
|
||||
|
||||
/// Reports the latest turn-scoped execution checklist.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct TurnPlanUpdatedNotification {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
#[serde(default)]
|
||||
pub explanation: Option<String>,
|
||||
pub plan: Vec<TurnPlanStep>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct TurnPlanStep {
|
||||
pub step: String,
|
||||
pub status: TurnPlanStepStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TurnPlanStepStatus {
|
||||
Pending,
|
||||
InProgress,
|
||||
Completed,
|
||||
}
|
||||
|
||||
/// A user-visible runtime warning projected from the current event stream.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -7,6 +7,9 @@ pub const V2_SCHEMA_TYPE_NAMES: &[&str] = &[
|
||||
"TextRange",
|
||||
"ConfigWarningNotification",
|
||||
"WarningNotification",
|
||||
"ErrorNotification",
|
||||
"SkillsChangedNotification",
|
||||
"McpServerOauthLoginCompletedNotification",
|
||||
"CurrentTimeReadParams",
|
||||
"CurrentTimeReadResponse",
|
||||
"AdditionalNetworkPermissions",
|
||||
@@ -72,6 +75,8 @@ pub const V2_SCHEMA_TYPE_NAMES: &[&str] = &[
|
||||
"Thread",
|
||||
"GitInfo",
|
||||
"Turn",
|
||||
"CodexErrorInfo",
|
||||
"NonSteerableTurnKind",
|
||||
"TurnError",
|
||||
"ThreadItem",
|
||||
"HookPromptFragment",
|
||||
@@ -197,6 +202,9 @@ pub const V2_SCHEMA_TYPE_NAMES: &[&str] = &[
|
||||
"ThreadClosedNotification",
|
||||
"TurnStartedNotification",
|
||||
"TurnCompletedNotification",
|
||||
"TurnPlanUpdatedNotification",
|
||||
"TurnPlanStep",
|
||||
"TurnPlanStepStatus",
|
||||
"ItemStartedNotification",
|
||||
"ItemCompletedNotification",
|
||||
"AgentMessageDeltaNotification",
|
||||
|
||||
@@ -550,6 +550,121 @@ fn warning_notification_round_trips_thread_scoped_lime_shape() {
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_notification_round_trips_exact_codex_shape() {
|
||||
let expected = json!({
|
||||
"method": "error",
|
||||
"params": {
|
||||
"error": {
|
||||
"message": "provider stream timed out",
|
||||
"codexErrorInfo": {
|
||||
"responseStreamDisconnected": {
|
||||
"httpStatusCode": null
|
||||
}
|
||||
},
|
||||
"additionalDetails": "request id: req-1"
|
||||
},
|
||||
"willRetry": false,
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1"
|
||||
}
|
||||
});
|
||||
|
||||
let notification: ServerNotification =
|
||||
serde_json::from_value(expected.clone()).expect("decode error notification");
|
||||
assert_eq!(notification.method(), METHOD_ERROR);
|
||||
let raw: crate::JsonRpcNotification = notification.clone().into();
|
||||
assert_eq!(raw.method, METHOD_ERROR);
|
||||
assert_eq!(
|
||||
ServerNotification::try_from(raw).expect("decode JSON-RPC error notification"),
|
||||
notification
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(notification).expect("encode error notification"),
|
||||
expected
|
||||
);
|
||||
assert!(NOTIFICATION_METHODS.contains(&METHOD_ERROR));
|
||||
|
||||
for malformed in [
|
||||
json!({
|
||||
"method": "error",
|
||||
"params": {
|
||||
"error": { "message": "failed" },
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1"
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"method": "error",
|
||||
"params": {
|
||||
"error": { "message": "failed" },
|
||||
"willRetry": "false",
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1"
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"method": "error",
|
||||
"params": {
|
||||
"error": { "message": "failed", "errorInfo": "legacy" },
|
||||
"willRetry": false,
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1"
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"method": "error",
|
||||
"params": {
|
||||
"error": { "message": "failed", "additionalDetails": 42 },
|
||||
"willRetry": false,
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1"
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"method": "error",
|
||||
"params": {
|
||||
"error": { "message": "failed" },
|
||||
"willRetry": false,
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1",
|
||||
"retryable": true
|
||||
}
|
||||
}),
|
||||
] {
|
||||
assert!(serde_json::from_value::<ServerNotification>(malformed).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skills_changed_notification_round_trips_empty_invalidation_shape() {
|
||||
let expected = json!({
|
||||
"method": "skills/changed",
|
||||
"params": {}
|
||||
});
|
||||
|
||||
let notification: ServerNotification =
|
||||
serde_json::from_value(expected.clone()).expect("decode skills changed notification");
|
||||
assert_eq!(notification.method(), METHOD_SKILLS_CHANGED);
|
||||
let raw: crate::JsonRpcNotification = notification.clone().into();
|
||||
assert_eq!(raw.method, METHOD_SKILLS_CHANGED);
|
||||
assert_eq!(raw.params, Some(json!({})));
|
||||
assert_eq!(
|
||||
ServerNotification::try_from(raw).expect("decode JSON-RPC skills changed notification"),
|
||||
notification
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(notification).expect("encode skills changed notification"),
|
||||
expected
|
||||
);
|
||||
assert!(NOTIFICATION_METHODS.contains(&METHOD_SKILLS_CHANGED));
|
||||
assert!(serde_json::from_value::<ServerNotification>(json!({
|
||||
"method": "skills/changed",
|
||||
"params": { "path": "/private/skill" }
|
||||
}))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_search_occurrences_round_trips_exact_codex_shape() {
|
||||
let expected = json!({
|
||||
@@ -1113,6 +1228,41 @@ fn plan_delta_notification_round_trips_codex_shape() {
|
||||
assert!(NOTIFICATION_METHODS.contains(&METHOD_PLAN_DELTA));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_plan_updated_notification_round_trips_codex_shape_and_rejects_unknown_fields() {
|
||||
let expected = json!({
|
||||
"method": "turn/plan/updated",
|
||||
"params": {
|
||||
"threadId": "thread_1",
|
||||
"turnId": "turn_2",
|
||||
"explanation": "继续执行",
|
||||
"plan": [
|
||||
{ "step": "读取现状", "status": "completed" },
|
||||
{ "step": "补齐主链", "status": "inProgress" }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let notification: ServerNotification =
|
||||
serde_json::from_value(expected.clone()).expect("decode turn plan update");
|
||||
assert_eq!(notification.method(), METHOD_TURN_PLAN_UPDATED);
|
||||
let raw: crate::JsonRpcNotification = notification.clone().into();
|
||||
assert_eq!(raw.method, METHOD_TURN_PLAN_UPDATED);
|
||||
assert_eq!(
|
||||
ServerNotification::try_from(raw).expect("decode JSON-RPC turn plan update"),
|
||||
notification
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(notification).expect("encode turn plan update"),
|
||||
expected
|
||||
);
|
||||
assert!(NOTIFICATION_METHODS.contains(&METHOD_TURN_PLAN_UPDATED));
|
||||
|
||||
let mut malformed = expected;
|
||||
malformed["params"]["plan"][0]["unexpected"] = json!(true);
|
||||
assert!(serde_json::from_value::<ServerNotification>(malformed).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_output_delta_notification_round_trips_codex_shape() {
|
||||
let expected = json!({
|
||||
@@ -2119,6 +2269,9 @@ fn typed_v2_envelope_schema_names_are_stable() {
|
||||
[
|
||||
"configWarning",
|
||||
"warning",
|
||||
"error",
|
||||
"skills/changed",
|
||||
"mcpServer/oauthLogin/completed",
|
||||
"thread/started",
|
||||
"thread/archived",
|
||||
"thread/deleted",
|
||||
@@ -2128,6 +2281,7 @@ fn typed_v2_envelope_schema_names_are_stable() {
|
||||
"thread/status/changed",
|
||||
"turn/started",
|
||||
"turn/completed",
|
||||
"turn/plan/updated",
|
||||
"item/started",
|
||||
"item/completed",
|
||||
"item/agentMessage/delta",
|
||||
|
||||
@@ -748,6 +748,11 @@ pub(super) fn v2_schemas() -> Vec<GeneratedJsonSchema> {
|
||||
typed_schema::<v2::TextRange>("TextRange"),
|
||||
typed_schema::<v2::ConfigWarningNotification>("ConfigWarningNotification"),
|
||||
typed_schema::<v2::WarningNotification>("WarningNotification"),
|
||||
typed_schema::<v2::ErrorNotification>("ErrorNotification"),
|
||||
typed_schema::<v2::SkillsChangedNotification>("SkillsChangedNotification"),
|
||||
typed_schema::<v2::McpServerOauthLoginCompletedNotification>(
|
||||
"McpServerOauthLoginCompletedNotification",
|
||||
),
|
||||
typed_schema::<v2::CurrentTimeReadParams>("CurrentTimeReadParams"),
|
||||
typed_schema::<v2::CurrentTimeReadResponse>("CurrentTimeReadResponse"),
|
||||
typed_schema::<v2::AdditionalNetworkPermissions>("AdditionalNetworkPermissions"),
|
||||
@@ -823,6 +828,8 @@ pub(super) fn v2_schemas() -> Vec<GeneratedJsonSchema> {
|
||||
typed_schema::<v2::Thread>("Thread"),
|
||||
typed_schema::<v2::GitInfo>("GitInfo"),
|
||||
typed_schema::<v2::Turn>("Turn"),
|
||||
typed_schema::<v2::CodexErrorInfo>("CodexErrorInfo"),
|
||||
typed_schema::<v2::NonSteerableTurnKind>("NonSteerableTurnKind"),
|
||||
typed_schema::<v2::TurnError>("TurnError"),
|
||||
typed_schema::<v2::ThreadItem>("ThreadItem"),
|
||||
typed_schema::<v2::HookPromptFragment>("HookPromptFragment"),
|
||||
@@ -972,6 +979,9 @@ pub(super) fn v2_schemas() -> Vec<GeneratedJsonSchema> {
|
||||
typed_schema::<v2::ThreadClosedNotification>("ThreadClosedNotification"),
|
||||
typed_schema::<v2::TurnStartedNotification>("TurnStartedNotification"),
|
||||
typed_schema::<v2::TurnCompletedNotification>("TurnCompletedNotification"),
|
||||
typed_schema::<v2::TurnPlanUpdatedNotification>("TurnPlanUpdatedNotification"),
|
||||
typed_schema::<v2::TurnPlanStep>("TurnPlanStep"),
|
||||
typed_schema::<v2::TurnPlanStepStatus>("TurnPlanStepStatus"),
|
||||
typed_schema::<v2::ItemStartedNotification>("ItemStartedNotification"),
|
||||
typed_schema::<v2::ItemCompletedNotification>("ItemCompletedNotification"),
|
||||
typed_schema::<v2::AgentMessageDeltaNotification>("AgentMessageDeltaNotification"),
|
||||
|
||||
@@ -37,6 +37,7 @@ lime-browser-runtime.workspace = true
|
||||
lime-media-runtime.workspace = true
|
||||
lime-mcp.workspace = true
|
||||
model-provider.workspace = true
|
||||
notify.workspace = true
|
||||
runtime-core.workspace = true
|
||||
lime-skills.workspace = true
|
||||
lime-services = { workspace = true, features = ["local-sensevoice"] }
|
||||
|
||||
@@ -35,6 +35,7 @@ mod runtime_backend;
|
||||
mod runtime_factory;
|
||||
mod server_request;
|
||||
mod skill_registry;
|
||||
mod skills_watcher;
|
||||
mod thread_listener;
|
||||
#[cfg(test)]
|
||||
mod thread_listener_tests;
|
||||
@@ -295,6 +296,7 @@ pub struct AppServer {
|
||||
server_requests: server_request::ServerRequestRouter,
|
||||
current_time_requests: current_time::CurrentTimeRequestRouter,
|
||||
mcp_elicitation_requests: mcp_elicitation::ElicitationRequestSource,
|
||||
_skills_watcher: Option<Arc<skills_watcher::SkillsWatcher>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -345,6 +347,7 @@ impl AppServer {
|
||||
};
|
||||
let interrupt_router = server_requests.clone();
|
||||
let notification_bridge = interrupt_bridge.clone();
|
||||
let skills_watcher_bridge = notification_bridge.clone();
|
||||
let turn_interrupt_hook: processor::TurnInterruptHook =
|
||||
Arc::new(move |thread_id, turn_id| {
|
||||
let bridge = interrupt_bridge.clone();
|
||||
@@ -364,6 +367,11 @@ impl AppServer {
|
||||
.await;
|
||||
})
|
||||
});
|
||||
let skills_watcher = if cfg!(test) {
|
||||
None
|
||||
} else {
|
||||
skills_watcher::SkillsWatcher::start_default(skills_watcher_bridge)
|
||||
};
|
||||
Self {
|
||||
processor: RequestProcessor::new_with_thread_states(runtime, thread_states.clone())
|
||||
.with_turn_interrupt_hook(turn_interrupt_hook)
|
||||
@@ -380,6 +388,7 @@ impl AppServer {
|
||||
server_requests,
|
||||
current_time_requests,
|
||||
mcp_elicitation_requests: mcp_elicitation::ElicitationRequestSource::default(),
|
||||
_skills_watcher: skills_watcher,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::super::*;
|
||||
use app_server_protocol::{McpServerOauthLoginParams, McpServerOauthLoginResponse};
|
||||
use app_server_protocol::McpServerOauthLoginParams;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
@@ -76,7 +76,7 @@ impl McpAppDataSource for LocalAppDataSource {
|
||||
async fn login_mcp_server_oauth(
|
||||
&self,
|
||||
params: McpServerOauthLoginParams,
|
||||
) -> Result<McpServerOauthLoginResponse, RuntimeCoreError> {
|
||||
) -> Result<lime_mcp::McpOAuthLoginHandle, RuntimeCoreError> {
|
||||
mcp::login_mcp_server_oauth(&self.db, &self.mcp_manager, params).await
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ use app_server_protocol::McpServerImportFromAppResponse;
|
||||
use app_server_protocol::McpServerLifecycleResponse;
|
||||
use app_server_protocol::McpServerListResponse;
|
||||
use app_server_protocol::McpServerOauthLoginParams;
|
||||
use app_server_protocol::McpServerOauthLoginResponse;
|
||||
use app_server_protocol::McpServerStartParams;
|
||||
use app_server_protocol::McpServerStatusListResponse;
|
||||
use app_server_protocol::McpServerStopParams;
|
||||
@@ -165,7 +164,7 @@ pub(crate) async fn login_mcp_server_oauth(
|
||||
db: &DbConnection,
|
||||
manager: &McpManagerState,
|
||||
params: McpServerOauthLoginParams,
|
||||
) -> Result<McpServerOauthLoginResponse, RuntimeCoreError> {
|
||||
) -> Result<lime_mcp::McpOAuthLoginHandle, RuntimeCoreError> {
|
||||
let server = McpService::get_all(db)
|
||||
.map_err(data_error)?
|
||||
.into_iter()
|
||||
@@ -184,10 +183,6 @@ pub(crate) async fn login_mcp_server_oauth(
|
||||
)
|
||||
.await
|
||||
.map_err(mcp_error)
|
||||
.map(|response| McpServerOauthLoginResponse {
|
||||
authorization_url: response.authorization_url,
|
||||
state: response.state,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn list_mcp_tools(
|
||||
|
||||
@@ -18,6 +18,7 @@ use app_server_protocol::METHOD_MCP_SERVER_STATUS_LIST;
|
||||
use app_server_protocol::METHOD_MCP_SERVER_STOP;
|
||||
use app_server_protocol::METHOD_MCP_TOOL_CALL;
|
||||
use app_server_protocol::METHOD_MCP_TOOL_LIST;
|
||||
use app_server_protocol::METHOD_SKILL_CACHE_REFRESH;
|
||||
use app_server_protocol::SERVER_NAME;
|
||||
use app_server_transport::decode_message;
|
||||
use lime_core::database::schema::create_tables;
|
||||
@@ -82,6 +83,55 @@ fn setup_data_source() -> LocalAppDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn successful_skill_catalog_mutation_emits_typed_invalidation() {
|
||||
let server = AppServer::with_runtime(
|
||||
RuntimeCore::with_backend(Arc::new(MockBackend))
|
||||
.with_app_data_source(Arc::new(setup_data_source())),
|
||||
);
|
||||
let init = app_server_request(
|
||||
&server,
|
||||
1,
|
||||
METHOD_INITIALIZE,
|
||||
json!({
|
||||
"clientInfo": { "name": "skills-changed-test", "version": "test" },
|
||||
"capabilities": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
init.pointer("/result/serverInfo/name"),
|
||||
Some(&json!(SERVER_NAME))
|
||||
);
|
||||
app_server_notification(&server, METHOD_INITIALIZED, json!({})).await;
|
||||
|
||||
let line = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": METHOD_SKILL_CACHE_REFRESH,
|
||||
"params": {}
|
||||
})
|
||||
.to_string();
|
||||
let responses = server
|
||||
.handle_json_line(&line)
|
||||
.await
|
||||
.expect("handle Skill catalog mutation");
|
||||
|
||||
assert_eq!(responses.len(), 2, "{responses:?}");
|
||||
let response: Value = serde_json::from_str(&responses[0]).expect("decode response");
|
||||
let notification = decode_message(&responses[1]).expect("decode notification");
|
||||
assert_eq!(response["result"], json!({ "success": true }));
|
||||
assert_eq!(
|
||||
notification,
|
||||
app_server_protocol::JsonRpcMessage::Notification(
|
||||
app_server_protocol::protocol::v2::ServerNotification::SkillsChanged(
|
||||
app_server_protocol::protocol::v2::SkillsChangedNotification {},
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn diagnostics_use_injected_agent_log_root() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
//! mcp domain handlers for the App Server processor.
|
||||
|
||||
use super::{dispatch_result, parse_params, to_jsonrpc_error, RequestProcessor, RpcDispatch};
|
||||
use app_server_protocol::protocol::v2::{
|
||||
McpServerOauthLoginCompletedNotification, ServerNotification,
|
||||
};
|
||||
use app_server_protocol::{
|
||||
JsonRpcError, McpPromptGetParams, McpResourceReadParams, McpResourceSubscribeParams,
|
||||
McpResourceUnsubscribeParams, McpServerCreateParams, McpServerDeleteParams,
|
||||
McpServerEnabledSetParams, McpServerImportFromAppParams, McpServerOauthLoginParams,
|
||||
McpServerStartParams, McpServerStopParams, McpServerUpdateParams, McpToolCallParams,
|
||||
McpToolCallWithCallerParams, McpToolListForContextParams, McpToolSearchParams,
|
||||
McpServerOauthLoginResponse, McpServerStartParams, McpServerStopParams, McpServerUpdateParams,
|
||||
McpToolCallParams, McpToolCallWithCallerParams, McpToolListForContextParams,
|
||||
McpToolSearchParams,
|
||||
};
|
||||
|
||||
impl RequestProcessor {
|
||||
@@ -148,11 +152,33 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: McpServerOauthLoginParams = parse_params(params)?;
|
||||
let response = self
|
||||
let server_name = params.name.clone();
|
||||
let handle = self
|
||||
.runtime
|
||||
.login_mcp_server_oauth(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
let response = McpServerOauthLoginResponse {
|
||||
authorization_url: handle.authorization_url.clone(),
|
||||
state: handle.state.clone(),
|
||||
};
|
||||
let processor = self.clone();
|
||||
tokio::spawn(async move {
|
||||
let (success, error) = match handle.wait().await {
|
||||
Ok(()) => (true, None),
|
||||
Err(error) => (false, Some(error.to_string())),
|
||||
};
|
||||
processor
|
||||
.publish_server_notification(ServerNotification::McpServerOauthLoginCompleted(
|
||||
McpServerOauthLoginCompletedNotification {
|
||||
name: server_name,
|
||||
thread_id: None,
|
||||
success,
|
||||
error,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
});
|
||||
dispatch_result(response)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! skill domain handlers for the App Server processor.
|
||||
|
||||
use super::{dispatch_result, parse_params, to_jsonrpc_error, RequestProcessor, RpcDispatch};
|
||||
use app_server_protocol::protocol::v2::{
|
||||
ServerNotification as V2ServerNotification, SkillsChangedNotification,
|
||||
};
|
||||
use app_server_protocol::{
|
||||
JsonRpcError, SkillDownloadInstallParams, SkillLocalDetailInspectParams,
|
||||
SkillLocalImportParams, SkillLocalInspectParams, SkillLocalRenameParams,
|
||||
@@ -10,6 +13,7 @@ use app_server_protocol::{
|
||||
SkillRemoteInspectParams, SkillRepositoryDeleteParams, SkillRepositorySaveParams,
|
||||
SkillScaffoldCreateParams,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
impl RequestProcessor {
|
||||
pub(super) async fn handle_skill_list_impl(&self) -> Result<RpcDispatch, JsonRpcError> {
|
||||
@@ -52,12 +56,13 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: SkillManagementInstallParams = parse_params(params)?;
|
||||
let changes_lime_catalog = is_lime_app(¶ms.app);
|
||||
let response = self
|
||||
.runtime
|
||||
.install_management_skill(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, changes_lime_catalog)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_management_uninstall_impl(
|
||||
@@ -66,12 +71,13 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: SkillManagementUninstallParams = parse_params(params)?;
|
||||
let changes_lime_catalog = is_lime_app(¶ms.app);
|
||||
let response = self
|
||||
.runtime
|
||||
.uninstall_management_skill(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, changes_lime_catalog)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_repository_list_impl(
|
||||
@@ -97,7 +103,7 @@ impl RequestProcessor {
|
||||
.save_skill_repository(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, true)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_repository_delete_impl(
|
||||
@@ -111,7 +117,7 @@ impl RequestProcessor {
|
||||
.delete_skill_repository(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, true)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_cache_refresh_impl(
|
||||
@@ -123,7 +129,7 @@ impl RequestProcessor {
|
||||
.refresh_skill_cache()
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, true)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_installed_directories_list_impl(
|
||||
@@ -186,12 +192,13 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: SkillScaffoldCreateParams = parse_params(params)?;
|
||||
let changes_lime_catalog = is_lime_app(¶ms.app);
|
||||
let response = self
|
||||
.runtime
|
||||
.create_skill_scaffold(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, changes_lime_catalog)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_local_import_impl(
|
||||
@@ -200,12 +207,13 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: SkillLocalImportParams = parse_params(params)?;
|
||||
let changes_lime_catalog = is_lime_app(¶ms.app);
|
||||
let response = self
|
||||
.runtime
|
||||
.import_local_skill(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, changes_lime_catalog)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_local_rename_impl(
|
||||
@@ -214,12 +222,13 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: SkillLocalRenameParams = parse_params(params)?;
|
||||
let changes_lime_catalog = is_lime_app(¶ms.app);
|
||||
let response = self
|
||||
.runtime
|
||||
.rename_local_skill(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, changes_lime_catalog)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_remote_inspect_impl(
|
||||
@@ -242,12 +251,13 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: SkillPackageLocalInstallParams = parse_params(params)?;
|
||||
let changes_lime_catalog = is_lime_app(¶ms.app);
|
||||
let response = self
|
||||
.runtime
|
||||
.install_local_skill_package(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, changes_lime_catalog)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_package_local_replace_impl(
|
||||
@@ -256,12 +266,13 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: SkillPackageLocalReplaceParams = parse_params(params)?;
|
||||
let changes_lime_catalog = is_lime_app(¶ms.app);
|
||||
let response = self
|
||||
.runtime
|
||||
.replace_local_skill_package(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, changes_lime_catalog)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_package_export_impl(
|
||||
@@ -284,12 +295,13 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: SkillMarketplaceInstallParams = parse_params(params)?;
|
||||
let changes_lime_catalog = is_lime_app(¶ms.app);
|
||||
let response = self
|
||||
.runtime
|
||||
.install_marketplace_skill(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, changes_lime_catalog)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_skill_download_install_impl(
|
||||
@@ -298,11 +310,30 @@ impl RequestProcessor {
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
self.ensure_initialized()?;
|
||||
let params: SkillDownloadInstallParams = parse_params(params)?;
|
||||
let changes_lime_catalog = is_lime_app(¶ms.app);
|
||||
let response = self
|
||||
.runtime
|
||||
.install_skill_from_download_url(params)
|
||||
.await
|
||||
.map_err(to_jsonrpc_error)?;
|
||||
dispatch_result(response)
|
||||
dispatch_skill_catalog_mutation(response, changes_lime_catalog)
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_skill_catalog_mutation(
|
||||
response: impl Serialize,
|
||||
changes_lime_catalog: bool,
|
||||
) -> Result<RpcDispatch, JsonRpcError> {
|
||||
let dispatch = dispatch_result(response)?;
|
||||
if !changes_lime_catalog {
|
||||
return Ok(dispatch);
|
||||
}
|
||||
lime_skills::invalidate_agent_skill_snapshot_cache();
|
||||
Ok(dispatch.with_notification(
|
||||
V2ServerNotification::SkillsChanged(SkillsChangedNotification {}).into(),
|
||||
))
|
||||
}
|
||||
|
||||
fn is_lime_app(app: &str) -> bool {
|
||||
app.trim().eq_ignore_ascii_case("lime")
|
||||
}
|
||||
|
||||
@@ -50,6 +50,44 @@ fn event_notifications_jsonrpc_emits_direct_agent_message_delta() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_notifications_jsonrpc_emits_direct_turn_plan_update() {
|
||||
let message = single_event_notification(AgentEvent {
|
||||
event_id: "evt_plan_1".to_string(),
|
||||
sequence: 2,
|
||||
session_id: "sess_1".to_string(),
|
||||
thread_id: Some("thread_1".to_string()),
|
||||
turn_id: Some("turn_1".to_string()),
|
||||
event_type: "turn.plan.updated".to_string(),
|
||||
timestamp: "2026-07-05T00:00:00Z".to_string(),
|
||||
payload: json!({
|
||||
"explanation": "继续执行",
|
||||
"plan": [
|
||||
{ "step": "读取现状", "status": "completed" },
|
||||
{ "step": "补齐主链", "status": "in_progress" }
|
||||
]
|
||||
}),
|
||||
})
|
||||
.expect("notification");
|
||||
|
||||
let JsonRpcMessage::Notification(notification) = message else {
|
||||
panic!("expected notification");
|
||||
};
|
||||
assert_eq!(notification.method, "turn/plan/updated");
|
||||
assert_eq!(
|
||||
notification.params.expect("params"),
|
||||
json!({
|
||||
"threadId": "thread_1",
|
||||
"turnId": "turn_1",
|
||||
"explanation": "继续执行",
|
||||
"plan": [
|
||||
{ "step": "读取现状", "status": "completed" },
|
||||
{ "step": "补齐主链", "status": "inProgress" }
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_notifications_jsonrpc_emits_typed_warning_with_localization_code() {
|
||||
let message = single_event_notification(AgentEvent {
|
||||
@@ -128,9 +166,215 @@ fn warning_alias_does_not_activate_typed_notification() {
|
||||
assert_eq!(notification.method, "agentSession/event");
|
||||
}
|
||||
|
||||
fn failed_turn_event(event_id: &str, message: &str) -> AgentEvent {
|
||||
AgentEvent {
|
||||
event_id: event_id.to_string(),
|
||||
sequence: 9,
|
||||
session_id: "sess_1".to_string(),
|
||||
thread_id: Some("thread_1".to_string()),
|
||||
turn_id: Some("turn_1".to_string()),
|
||||
event_type: "turn.failed".to_string(),
|
||||
timestamp: "2026-07-05T00:00:01Z".to_string(),
|
||||
payload: json!({
|
||||
"message": message,
|
||||
"turn": {
|
||||
"sessionId": "sess_1",
|
||||
"threadId": "thread_1",
|
||||
"turnId": "turn_1",
|
||||
"status": "failed",
|
||||
"createdAtMs": 100,
|
||||
"updatedAtMs": 120,
|
||||
"startedAtMs": 100,
|
||||
"completedAtMs": 120,
|
||||
"error": {"message": message}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_notifications_jsonrpc_lowers_turn_failed_to_direct_completion() {
|
||||
let message = single_event_notification(AgentEvent {
|
||||
fn event_notifications_project_retry_and_terminal_errors_with_exact_semantics() {
|
||||
let mut projector = V2NotificationProjector::default();
|
||||
let retry = project_event_notifications_jsonrpc(
|
||||
&mut projector,
|
||||
AgentEvent {
|
||||
event_id: "evt_retry".to_string(),
|
||||
sequence: 7,
|
||||
session_id: "sess_1".to_string(),
|
||||
thread_id: Some("thread_1".to_string()),
|
||||
turn_id: Some("turn_1".to_string()),
|
||||
event_type: "plugin_worker.retry".to_string(),
|
||||
timestamp: "2026-07-05T00:00:00Z".to_string(),
|
||||
payload: json!({
|
||||
"message": "provider stream reconnecting",
|
||||
"errorCode": "PLUGIN_WORKER_RETRYABLE_FAILURE",
|
||||
"retryable": false
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("retry notification");
|
||||
assert_eq!(retry.len(), 1);
|
||||
let JsonRpcMessage::Notification(retry) = &retry[0] else {
|
||||
panic!("expected retry notification");
|
||||
};
|
||||
assert_eq!(retry.method, "error");
|
||||
assert_eq!(retry.params.as_ref().expect("params")["willRetry"], true);
|
||||
|
||||
let terminal = project_event_notifications_jsonrpc(
|
||||
&mut projector,
|
||||
failed_turn_event("evt_failed_after_retry", "retry budget exhausted"),
|
||||
)
|
||||
.expect("terminal notifications");
|
||||
let terminal = terminal
|
||||
.into_iter()
|
||||
.map(|message| match message {
|
||||
JsonRpcMessage::Notification(notification) => notification,
|
||||
other => panic!("expected notification, got {other:?}"),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
terminal
|
||||
.iter()
|
||||
.map(|notification| notification.method.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["error", "turn/completed"]
|
||||
);
|
||||
assert_eq!(
|
||||
terminal[0].params.as_ref().expect("error params")["willRetry"],
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
terminal[1].params.as_ref().expect("completion params")["turn"]["status"],
|
||||
"failed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_error_is_terminal_even_when_legacy_retryable_is_true_and_is_not_duplicated() {
|
||||
let mut projector = V2NotificationProjector::default();
|
||||
let runtime_error = project_event_notifications_jsonrpc(
|
||||
&mut projector,
|
||||
AgentEvent {
|
||||
event_id: "evt_runtime_error".to_string(),
|
||||
sequence: 8,
|
||||
session_id: "sess_1".to_string(),
|
||||
thread_id: Some("thread_1".to_string()),
|
||||
turn_id: Some("turn_1".to_string()),
|
||||
event_type: "runtime.error".to_string(),
|
||||
timestamp: "2026-07-05T00:00:00Z".to_string(),
|
||||
payload: json!({
|
||||
"message": "retry budget exhausted",
|
||||
"errorCode": "PLUGIN_WORKER_RETRYABLE_FAILURE",
|
||||
"retryable": true
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("runtime error notification");
|
||||
let JsonRpcMessage::Notification(runtime_error) = &runtime_error[0] else {
|
||||
panic!("expected runtime error notification");
|
||||
};
|
||||
let params = runtime_error.params.as_ref().expect("runtime error params");
|
||||
assert_eq!(runtime_error.method, "error");
|
||||
assert_eq!(params["willRetry"], false);
|
||||
assert_eq!(params["error"]["codexErrorInfo"], "other");
|
||||
|
||||
let completion = project_event_notifications_jsonrpc(
|
||||
&mut projector,
|
||||
failed_turn_event("evt_failed_after_runtime_error", "retry budget exhausted"),
|
||||
)
|
||||
.expect("completion after terminal error");
|
||||
assert_eq!(completion.len(), 1);
|
||||
let JsonRpcMessage::Notification(completion) = &completion[0] else {
|
||||
panic!("expected turn completion");
|
||||
};
|
||||
assert_eq!(completion.method, "turn/completed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_failed_without_runtime_error_emits_error_before_completion() {
|
||||
let messages = event_notifications_jsonrpc(failed_turn_event(
|
||||
"evt_failed_without_error",
|
||||
"provider stream timed out",
|
||||
))
|
||||
.expect("failed turn notifications");
|
||||
let methods = messages
|
||||
.iter()
|
||||
.map(|message| match message {
|
||||
JsonRpcMessage::Notification(notification) => notification.method.as_str(),
|
||||
other => panic!("expected notification, got {other:?}"),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(methods, ["error", "turn/completed"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_typed_error_sources_fail_closed() {
|
||||
for (index, event) in [
|
||||
AgentEvent {
|
||||
event_id: "missing_thread".to_string(),
|
||||
sequence: 1,
|
||||
session_id: "sess_1".to_string(),
|
||||
thread_id: None,
|
||||
turn_id: Some("turn_1".to_string()),
|
||||
event_type: "runtime.error".to_string(),
|
||||
timestamp: "2026-07-05T00:00:00Z".to_string(),
|
||||
payload: json!({"message": "failed"}),
|
||||
},
|
||||
AgentEvent {
|
||||
event_id: "missing_turn".to_string(),
|
||||
sequence: 1,
|
||||
session_id: "sess_1".to_string(),
|
||||
thread_id: Some("thread_1".to_string()),
|
||||
turn_id: None,
|
||||
event_type: "runtime.error".to_string(),
|
||||
timestamp: "2026-07-05T00:00:00Z".to_string(),
|
||||
payload: json!({"message": "failed"}),
|
||||
},
|
||||
AgentEvent {
|
||||
event_id: "missing_message".to_string(),
|
||||
sequence: 1,
|
||||
session_id: "sess_1".to_string(),
|
||||
thread_id: Some("thread_1".to_string()),
|
||||
turn_id: Some("turn_1".to_string()),
|
||||
event_type: "runtime.error".to_string(),
|
||||
timestamp: "2026-07-05T00:00:00Z".to_string(),
|
||||
payload: json!({}),
|
||||
},
|
||||
AgentEvent {
|
||||
event_id: "invalid_retry".to_string(),
|
||||
sequence: 1,
|
||||
session_id: "sess_1".to_string(),
|
||||
thread_id: Some("thread_1".to_string()),
|
||||
turn_id: Some("turn_1".to_string()),
|
||||
event_type: "runtime.error".to_string(),
|
||||
timestamp: "2026-07-05T00:00:00Z".to_string(),
|
||||
payload: json!({"message": "failed", "willRetry": "false"}),
|
||||
},
|
||||
AgentEvent {
|
||||
event_id: "invalid_details".to_string(),
|
||||
sequence: 1,
|
||||
session_id: "sess_1".to_string(),
|
||||
thread_id: Some("thread_1".to_string()),
|
||||
turn_id: Some("turn_1".to_string()),
|
||||
event_type: "runtime.error".to_string(),
|
||||
timestamp: "2026-07-05T00:00:00Z".to_string(),
|
||||
payload: json!({"message": "failed", "additionalDetails": 42}),
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let error = match event_notifications_jsonrpc(event) {
|
||||
Ok(messages) => panic!("case {index} projected {messages:?}"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code, app_server_protocol::error_codes::RUNTIME_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_notifications_jsonrpc_lowers_turn_failed_to_error_then_completion() {
|
||||
let messages = event_notifications_jsonrpc(AgentEvent {
|
||||
event_id: "evt_failed".to_string(),
|
||||
sequence: 2,
|
||||
session_id: "sess_1".to_string(),
|
||||
@@ -153,13 +397,14 @@ fn event_notifications_jsonrpc_lowers_turn_failed_to_direct_completion() {
|
||||
}
|
||||
}),
|
||||
})
|
||||
.expect("notification");
|
||||
.expect("notifications");
|
||||
|
||||
let JsonRpcMessage::Notification(notification) = message else {
|
||||
assert_eq!(messages.len(), 2);
|
||||
let JsonRpcMessage::Notification(notification) = &messages[1] else {
|
||||
panic!("expected notification");
|
||||
};
|
||||
assert_eq!(notification.method, "turn/completed");
|
||||
let params = notification.params.expect("params");
|
||||
let params = notification.params.as_ref().expect("params");
|
||||
assert_eq!(params["threadId"], "thread_1");
|
||||
assert_eq!(params["turn"]["id"], "turn_1");
|
||||
assert_eq!(params["turn"]["status"], "failed");
|
||||
|
||||
@@ -1,18 +1,99 @@
|
||||
//! mcp request processor tests.
|
||||
|
||||
use super::super::*;
|
||||
use super::tests_support::initialize_processor;
|
||||
use app_server_protocol::{
|
||||
ClientCapabilities, JsonRpcMessage, RequestId, METHOD_INITIALIZE, METHOD_INITIALIZED,
|
||||
METHOD_MCP_PROMPT_GET, METHOD_MCP_PROMPT_LIST, METHOD_MCP_RESOURCE_LIST,
|
||||
METHOD_MCP_RESOURCE_READ, METHOD_MCP_RESOURCE_SUBSCRIBE, METHOD_MCP_RESOURCE_UNSUBSCRIBE,
|
||||
METHOD_MCP_SERVER_CREATE, METHOD_MCP_SERVER_DELETE, METHOD_MCP_SERVER_ENABLED_SET,
|
||||
METHOD_MCP_SERVER_IMPORT_FROM_APP, METHOD_MCP_SERVER_LIST, METHOD_MCP_SERVER_START,
|
||||
METHOD_MCP_SERVER_STATUS_LIST, METHOD_MCP_SERVER_STOP, METHOD_MCP_SERVER_SYNC_ALL_TO_LIVE,
|
||||
METHOD_MCP_SERVER_UPDATE, METHOD_MCP_TOOL_CALL, METHOD_MCP_TOOL_CALL_WITH_CALLER,
|
||||
METHOD_MCP_TOOL_LIST,
|
||||
METHOD_MCP_SERVER_IMPORT_FROM_APP, METHOD_MCP_SERVER_LIST, METHOD_MCP_SERVER_OAUTH_LOGIN,
|
||||
METHOD_MCP_SERVER_START, METHOD_MCP_SERVER_STATUS_LIST, METHOD_MCP_SERVER_STOP,
|
||||
METHOD_MCP_SERVER_SYNC_ALL_TO_LIVE, METHOD_MCP_SERVER_UPDATE, METHOD_MCP_TOOL_CALL,
|
||||
METHOD_MCP_TOOL_CALL_WITH_CALLER, METHOD_MCP_TOOL_LIST,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Default)]
|
||||
struct OAuthTestDataSource {
|
||||
login: std::sync::Mutex<Option<lime_mcp::McpOAuthLoginHandle>>,
|
||||
}
|
||||
|
||||
impl crate::SessionAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::WorkspaceAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::SkillAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::WorkspaceSkillBindingAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::GatewayAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::MediaAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::VoiceAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::PluginDataSource for OAuthTestDataSource {}
|
||||
impl crate::KnowledgeAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::AutomationOverviewAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::AutomationManagementAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::MemoryAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::DiagnosticsAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::UsageStatsAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::ModelProviderAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::ConnectAppDataSource for OAuthTestDataSource {}
|
||||
impl crate::RightSurfaceAppDataSource for OAuthTestDataSource {}
|
||||
|
||||
#[async_trait]
|
||||
impl crate::McpAppDataSource for OAuthTestDataSource {
|
||||
async fn login_mcp_server_oauth(
|
||||
&self,
|
||||
_params: app_server_protocol::McpServerOauthLoginParams,
|
||||
) -> Result<lime_mcp::McpOAuthLoginHandle, RuntimeCoreError> {
|
||||
self.login
|
||||
.lock()
|
||||
.expect("OAuth test login mutex poisoned")
|
||||
.take()
|
||||
.ok_or_else(|| RuntimeCoreError::Backend("OAuth test login missing".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn oauth_test_data_source() -> (
|
||||
Arc<OAuthTestDataSource>,
|
||||
tokio::sync::oneshot::Sender<Result<(), String>>,
|
||||
) {
|
||||
let (completion_tx, completion_rx) = tokio::sync::oneshot::channel::<Result<(), String>>();
|
||||
let handle = lime_mcp::McpOAuthLoginHandle::new(
|
||||
"https://auth.example/authorize",
|
||||
"pending",
|
||||
async move {
|
||||
completion_rx
|
||||
.await
|
||||
.map_err(|error| lime_mcp::McpError::ProtocolError(error.to_string()))?
|
||||
.map_err(lime_mcp::McpError::ConfigError)
|
||||
},
|
||||
);
|
||||
(
|
||||
Arc::new(OAuthTestDataSource {
|
||||
login: std::sync::Mutex::new(Some(handle)),
|
||||
}),
|
||||
completion_tx,
|
||||
)
|
||||
}
|
||||
|
||||
async fn start_oauth_test() -> (
|
||||
RequestProcessor,
|
||||
tokio::sync::oneshot::Sender<Result<(), String>>,
|
||||
tokio::sync::mpsc::UnboundedReceiver<app_server_protocol::protocol::v2::ServerNotification>,
|
||||
) {
|
||||
let (data_source, completion_tx) = oauth_test_data_source();
|
||||
let runtime = RuntimeCore::default().with_app_data_source(data_source);
|
||||
let (notification_tx, notification_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let hook: ServerNotificationHook = Arc::new(move |notification| {
|
||||
let notification_tx = notification_tx.clone();
|
||||
Box::pin(async move {
|
||||
let _ = notification_tx.send(notification);
|
||||
})
|
||||
});
|
||||
let processor = RequestProcessor::new(runtime).with_server_notification_hook(hook);
|
||||
initialize_processor(&processor).await;
|
||||
(processor, completion_tx, notification_rx)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_list_methods_require_initialized_and_return_current_empty_state() {
|
||||
let processor = RequestProcessor::new(RuntimeCore::default());
|
||||
@@ -232,3 +313,79 @@ async fn mcp_runtime_methods_require_initialized_and_fail_closed_without_manager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_oauth_login_returns_before_typed_success_notification() {
|
||||
let (processor, completion_tx, mut notification_rx) = start_oauth_test().await;
|
||||
let messages = processor
|
||||
.handle_request(JsonRpcRequest::new(
|
||||
RequestId::Integer(30),
|
||||
METHOD_MCP_SERVER_OAUTH_LOGIN,
|
||||
Some(json!({ "name": "remote-docs" })),
|
||||
))
|
||||
.await
|
||||
.expect("OAuth login response");
|
||||
|
||||
let [JsonRpcMessage::Response(response)] = messages.as_slice() else {
|
||||
panic!("expected OAuth login response, got {messages:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
response.result["authorizationUrl"],
|
||||
"https://auth.example/authorize"
|
||||
);
|
||||
assert_eq!(response.result["state"], "pending");
|
||||
|
||||
completion_tx.send(Ok(())).expect("complete OAuth login");
|
||||
let notification =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), notification_rx.recv())
|
||||
.await
|
||||
.expect("OAuth success notification timeout")
|
||||
.expect("OAuth success notification channel closed");
|
||||
assert_eq!(
|
||||
notification,
|
||||
app_server_protocol::protocol::v2::ServerNotification::McpServerOauthLoginCompleted(
|
||||
app_server_protocol::protocol::v2::McpServerOauthLoginCompletedNotification {
|
||||
name: "remote-docs".to_string(),
|
||||
thread_id: None,
|
||||
success: true,
|
||||
error: None,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_oauth_login_publishes_typed_failure_notification() {
|
||||
let (processor, completion_tx, mut notification_rx) = start_oauth_test().await;
|
||||
let messages = processor
|
||||
.handle_request(JsonRpcRequest::new(
|
||||
RequestId::Integer(31),
|
||||
METHOD_MCP_SERVER_OAUTH_LOGIN,
|
||||
Some(json!({ "name": "remote-docs" })),
|
||||
))
|
||||
.await
|
||||
.expect("OAuth login response");
|
||||
assert!(matches!(messages.as_slice(), [JsonRpcMessage::Response(_)]));
|
||||
|
||||
completion_tx
|
||||
.send(Err("scope rejected".to_string()))
|
||||
.expect("fail OAuth login");
|
||||
let notification =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), notification_rx.recv())
|
||||
.await
|
||||
.expect("OAuth failure notification timeout")
|
||||
.expect("OAuth failure notification channel closed");
|
||||
let app_server_protocol::protocol::v2::ServerNotification::McpServerOauthLoginCompleted(
|
||||
notification,
|
||||
) = notification
|
||||
else {
|
||||
panic!("expected OAuth completion notification");
|
||||
};
|
||||
assert_eq!(notification.name, "remote-docs");
|
||||
assert_eq!(notification.thread_id, None);
|
||||
assert!(!notification.success);
|
||||
assert!(notification
|
||||
.error
|
||||
.as_deref()
|
||||
.is_some_and(|error| error.contains("scope rejected")));
|
||||
}
|
||||
|
||||
@@ -296,7 +296,9 @@ fn project_turn(turn: canonical::Turn) -> Result<v2::Turn, JsonRpcError> {
|
||||
status: project_turn_status(turn.status),
|
||||
error: turn.error.map(|error| v2::TurnError {
|
||||
message: error.message,
|
||||
error_info: error.code.map(Value::String),
|
||||
codex_error_info: crate::processor::v2_notifications::error::codex_error_info_from_code(
|
||||
error.code.as_deref(),
|
||||
),
|
||||
additional_details: error.details,
|
||||
}),
|
||||
started_at: turn.started_at_ms.map(millis_to_seconds),
|
||||
|
||||
@@ -6,10 +6,12 @@ use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
|
||||
mod command;
|
||||
pub(crate) mod error;
|
||||
mod file_change;
|
||||
mod mcp;
|
||||
mod plan;
|
||||
mod thread_status;
|
||||
mod turn_plan;
|
||||
mod warning;
|
||||
|
||||
enum EventProjection {
|
||||
@@ -31,6 +33,7 @@ pub(crate) struct V2NotificationProjector {
|
||||
completed_file_change_item_ids: HashSet<String>,
|
||||
started_mcp_item_ids: HashSet<String>,
|
||||
completed_mcp_item_ids: HashSet<String>,
|
||||
terminal_error_turn_ids: HashSet<String>,
|
||||
thread_status: thread_status::ThreadStatusProjector,
|
||||
}
|
||||
|
||||
@@ -54,12 +57,12 @@ impl V2NotificationProjector {
|
||||
"turn.completed" => {
|
||||
return self.project_turn_completed_with_usage(event, v2::TurnStatus::Completed)
|
||||
}
|
||||
"turn.failed" => {
|
||||
return self.project_turn_completed_with_usage(event, v2::TurnStatus::Failed)
|
||||
}
|
||||
"turn.failed" => return self.project_turn_failed(event),
|
||||
"turn.canceled" => {
|
||||
return self.project_turn_completed_with_usage(event, v2::TurnStatus::Interrupted)
|
||||
}
|
||||
"plugin_worker.retry" => return self.project_error(event, Some(true)),
|
||||
"runtime.error" => return self.project_error(event, None),
|
||||
"action.required" => {
|
||||
return EventProjection::Direct(
|
||||
self.thread_status
|
||||
@@ -120,6 +123,7 @@ impl V2NotificationProjector {
|
||||
event,
|
||||
)
|
||||
}
|
||||
"turn.plan.updated" => return turn_plan::project(event),
|
||||
"tool.progress" => {
|
||||
return mcp::project_progress(
|
||||
&self.started_mcp_item_ids,
|
||||
@@ -233,6 +237,42 @@ impl V2NotificationProjector {
|
||||
EventProjection::Direct(notifications)
|
||||
}
|
||||
|
||||
fn project_error(
|
||||
&mut self,
|
||||
event: &AgentEvent,
|
||||
forced_will_retry: Option<bool>,
|
||||
) -> EventProjection {
|
||||
let Some(projected) = error::project(event, forced_will_retry) else {
|
||||
return EventProjection::Reject(projection_error(event));
|
||||
};
|
||||
if !projected.will_retry
|
||||
&& !self
|
||||
.terminal_error_turn_ids
|
||||
.insert(projected.turn_id.clone())
|
||||
{
|
||||
return EventProjection::Direct(Vec::new());
|
||||
}
|
||||
EventProjection::Direct(vec![projected.notification.into()])
|
||||
}
|
||||
|
||||
fn project_turn_failed(&mut self, event: &AgentEvent) -> EventProjection {
|
||||
let Some(turn_id) = required_event_id(event.turn_id.as_deref()) else {
|
||||
return EventProjection::Reject(projection_error(event));
|
||||
};
|
||||
let completion = self.project_turn_completed_with_usage(event, v2::TurnStatus::Failed);
|
||||
let EventProjection::Direct(mut notifications) = completion else {
|
||||
return completion;
|
||||
};
|
||||
if self.terminal_error_turn_ids.remove(&turn_id) {
|
||||
return EventProjection::Direct(notifications);
|
||||
}
|
||||
let Some(projected) = error::project(event, Some(false)) else {
|
||||
return EventProjection::Reject(projection_error(event));
|
||||
};
|
||||
notifications.insert(0, projected.notification.into());
|
||||
EventProjection::Direct(notifications)
|
||||
}
|
||||
|
||||
fn project_item(&mut self, event: &AgentEvent, completed: bool) -> Option<ServerNotification> {
|
||||
let thread_id = required_event_id(event.thread_id.as_deref())?;
|
||||
let turn_id = required_event_id(event.turn_id.as_deref())?;
|
||||
@@ -880,35 +920,44 @@ mod tests {
|
||||
(
|
||||
"turn.completed",
|
||||
json!({"turn": canonical_turn("completed")}),
|
||||
"turn/completed",
|
||||
vec!["turn/completed"],
|
||||
),
|
||||
(
|
||||
"turn.failed",
|
||||
json!({"turn": canonical_turn("failed")}),
|
||||
"turn/completed",
|
||||
json!({
|
||||
"message": "provider failed",
|
||||
"turn": canonical_turn("failed")
|
||||
}),
|
||||
vec!["error", "turn/completed"],
|
||||
),
|
||||
(
|
||||
"turn.canceled",
|
||||
json!({"turn": canonical_turn("interrupted")}),
|
||||
"turn/completed",
|
||||
vec!["turn/completed"],
|
||||
),
|
||||
(
|
||||
"item.started",
|
||||
json!({"item": canonical_item("inProgress")}),
|
||||
"item/started",
|
||||
vec!["item/started"],
|
||||
),
|
||||
(
|
||||
"item.completed",
|
||||
json!({"item": canonical_item("completed")}),
|
||||
"item/completed",
|
||||
vec!["item/completed"],
|
||||
),
|
||||
];
|
||||
for (event_type, payload, method) in cases {
|
||||
for (event_type, payload, methods) in cases {
|
||||
let mut projector = V2NotificationProjector::default();
|
||||
let notifications = projector
|
||||
.project(event(event_type, payload))
|
||||
.expect("direct lifecycle");
|
||||
assert_eq!(notifications[0].method, method);
|
||||
assert_eq!(
|
||||
notifications
|
||||
.iter()
|
||||
.map(|notification| notification.method.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
methods
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
use app_server_protocol::protocol::v2::{
|
||||
CodexErrorInfo, ErrorNotification, ServerNotification, TurnError,
|
||||
};
|
||||
use app_server_protocol::AgentEvent;
|
||||
use serde_json::Value;
|
||||
|
||||
pub(super) struct ProjectedError {
|
||||
pub(super) notification: ServerNotification,
|
||||
pub(super) turn_id: String,
|
||||
pub(super) will_retry: bool,
|
||||
}
|
||||
|
||||
pub(super) fn project(
|
||||
event: &AgentEvent,
|
||||
forced_will_retry: Option<bool>,
|
||||
) -> Option<ProjectedError> {
|
||||
let thread_id = required_id(event.thread_id.as_deref())?;
|
||||
let turn_id = required_id(event.turn_id.as_deref())?;
|
||||
let message = error_message(&event.payload)?;
|
||||
let additional_details = optional_string(
|
||||
&event.payload,
|
||||
&["additionalDetails", "additional_details", "details"],
|
||||
)
|
||||
.or_else(|| nested_turn_error_string(&event.payload, "additionalDetails"))
|
||||
.or_else(|| nested_turn_error_string(&event.payload, "details"));
|
||||
if has_invalid_optional_string(
|
||||
&event.payload,
|
||||
&["additionalDetails", "additional_details", "details"],
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let will_retry = match forced_will_retry {
|
||||
Some(will_retry) => will_retry,
|
||||
None => explicit_will_retry(&event.payload)?,
|
||||
};
|
||||
let code = payload_string(&event.payload, &["errorCode", "error_code", "code"])
|
||||
.or_else(|| nested_turn_error_string(&event.payload, "code"));
|
||||
|
||||
Some(ProjectedError {
|
||||
notification: ServerNotification::Error(ErrorNotification {
|
||||
error: TurnError {
|
||||
message,
|
||||
codex_error_info: codex_error_info_from_code(code.as_deref()),
|
||||
additional_details,
|
||||
},
|
||||
will_retry,
|
||||
thread_id,
|
||||
turn_id: turn_id.clone(),
|
||||
}),
|
||||
turn_id,
|
||||
will_retry,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn codex_error_info_from_code(code: Option<&str>) -> Option<CodexErrorInfo> {
|
||||
let code = code?;
|
||||
let normalized = code
|
||||
.chars()
|
||||
.filter(|character| character.is_ascii_alphanumeric())
|
||||
.flat_map(char::to_lowercase)
|
||||
.collect::<String>();
|
||||
Some(match normalized.as_str() {
|
||||
"contextwindowexceeded" => CodexErrorInfo::ContextWindowExceeded,
|
||||
"sessionbudgetexceeded" => CodexErrorInfo::SessionBudgetExceeded,
|
||||
"usagelimitexceeded" => CodexErrorInfo::UsageLimitExceeded,
|
||||
"serveroverloaded" => CodexErrorInfo::ServerOverloaded,
|
||||
"cyberpolicy" => CodexErrorInfo::CyberPolicy,
|
||||
"httpconnectionfailed" => CodexErrorInfo::HttpConnectionFailed {
|
||||
http_status_code: None,
|
||||
},
|
||||
"responsestreamconnectionfailed" => CodexErrorInfo::ResponseStreamConnectionFailed {
|
||||
http_status_code: None,
|
||||
},
|
||||
"internalservererror" => CodexErrorInfo::InternalServerError,
|
||||
"unauthorized" => CodexErrorInfo::Unauthorized,
|
||||
"badrequest" => CodexErrorInfo::BadRequest,
|
||||
"threadrollbackfailed" => CodexErrorInfo::ThreadRollbackFailed,
|
||||
"sandboxerror" => CodexErrorInfo::SandboxError,
|
||||
"responsestreamdisconnected" => CodexErrorInfo::ResponseStreamDisconnected {
|
||||
http_status_code: None,
|
||||
},
|
||||
"responsetoomanyfailedattempts" => CodexErrorInfo::ResponseTooManyFailedAttempts {
|
||||
http_status_code: None,
|
||||
},
|
||||
_ => CodexErrorInfo::Other,
|
||||
})
|
||||
}
|
||||
|
||||
fn error_message(payload: &Value) -> Option<String> {
|
||||
payload_string(payload, &["message", "errorMessage", "error_message"])
|
||||
.or_else(|| payload_string(payload, &["error"]))
|
||||
.or_else(|| nested_error_string(payload, "message"))
|
||||
.or_else(|| nested_turn_error_string(payload, "message"))
|
||||
}
|
||||
|
||||
fn explicit_will_retry(payload: &Value) -> Option<bool> {
|
||||
let mut result = None;
|
||||
for key in ["willRetry", "will_retry"] {
|
||||
let Some(value) = payload.get(key) else {
|
||||
continue;
|
||||
};
|
||||
let value = value.as_bool()?;
|
||||
if result.is_some_and(|current| current != value) {
|
||||
return None;
|
||||
}
|
||||
result = Some(value);
|
||||
}
|
||||
Some(result.unwrap_or(false))
|
||||
}
|
||||
|
||||
fn required_id(value: Option<&str>) -> Option<String> {
|
||||
value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn payload_string(payload: &Value, keys: &[&str]) -> Option<String> {
|
||||
keys.iter().find_map(|key| {
|
||||
payload
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
})
|
||||
}
|
||||
|
||||
fn optional_string(payload: &Value, keys: &[&str]) -> Option<String> {
|
||||
keys.iter().find_map(|key| match payload.get(key) {
|
||||
Some(Value::String(value)) => Some(value.clone()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn has_invalid_optional_string(payload: &Value, keys: &[&str]) -> bool {
|
||||
keys.iter().any(|key| {
|
||||
!matches!(
|
||||
payload.get(key),
|
||||
None | Some(Value::Null) | Some(Value::String(_))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn nested_error_string(payload: &Value, key: &str) -> Option<String> {
|
||||
payload
|
||||
.get("error")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|error| error.get(key))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn nested_turn_error_string(payload: &Value, key: &str) -> Option<String> {
|
||||
payload
|
||||
.get("turn")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|turn| turn.get("error"))
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|error| error.get(key))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use super::{projection_error, required_event_id, EventProjection};
|
||||
use app_server_protocol::protocol::v2::{
|
||||
self, ServerNotification, TurnPlanStep, TurnPlanStepStatus,
|
||||
};
|
||||
use app_server_protocol::AgentEvent;
|
||||
use serde_json::Value;
|
||||
use tool_runtime::update_plan::{PlanStep, PlanStepStatus};
|
||||
|
||||
pub(super) fn project(event: &AgentEvent) -> EventProjection {
|
||||
let Some(thread_id) = required_event_id(event.thread_id.as_deref()) else {
|
||||
return EventProjection::Reject(projection_error(event));
|
||||
};
|
||||
let Some(turn_id) = required_event_id(event.turn_id.as_deref()) else {
|
||||
return EventProjection::Reject(projection_error(event));
|
||||
};
|
||||
let Some(plan_value) = event.payload.get("plan") else {
|
||||
return EventProjection::Reject(projection_error(event));
|
||||
};
|
||||
let Ok(plan) = serde_json::from_value::<Vec<PlanStep>>(plan_value.clone()) else {
|
||||
return EventProjection::Reject(projection_error(event));
|
||||
};
|
||||
let explanation = match event.payload.get("explanation") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(value)) => Some(value.clone()),
|
||||
Some(_) => return EventProjection::Reject(projection_error(event)),
|
||||
};
|
||||
|
||||
EventProjection::Direct(vec![ServerNotification::TurnPlanUpdated(
|
||||
v2::TurnPlanUpdatedNotification {
|
||||
thread_id,
|
||||
turn_id,
|
||||
explanation,
|
||||
plan: plan.into_iter().map(project_step).collect(),
|
||||
},
|
||||
)
|
||||
.into()])
|
||||
}
|
||||
|
||||
fn project_step(step: PlanStep) -> TurnPlanStep {
|
||||
TurnPlanStep {
|
||||
step: step.step,
|
||||
status: match step.status {
|
||||
PlanStepStatus::Pending => TurnPlanStepStatus::Pending,
|
||||
PlanStepStatus::InProgress => TurnPlanStepStatus::InProgress,
|
||||
PlanStepStatus::Completed => TurnPlanStepStatus::Completed,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn event(payload: Value) -> AgentEvent {
|
||||
AgentEvent {
|
||||
event_id: "evt-plan-1".to_string(),
|
||||
sequence: 3,
|
||||
session_id: "session-plan-1".to_string(),
|
||||
thread_id: Some("thread-plan-1".to_string()),
|
||||
turn_id: Some("turn-plan-1".to_string()),
|
||||
event_type: "turn.plan.updated".to_string(),
|
||||
timestamp: "2026-07-31T00:00:00.000Z".to_string(),
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projects_update_plan_checklist_without_plan_item_lifecycle() {
|
||||
let EventProjection::Direct(notifications) = project(&event(json!({
|
||||
"explanation": "继续实现",
|
||||
"plan": [
|
||||
{ "step": "读现状", "status": "completed" },
|
||||
{ "step": "补主链", "status": "in_progress" }
|
||||
]
|
||||
}))) else {
|
||||
panic!("turn plan update should project directly");
|
||||
};
|
||||
|
||||
assert_eq!(notifications.len(), 1);
|
||||
assert_eq!(notifications[0].method, "turn/plan/updated");
|
||||
let params = notifications[0].params.as_ref().expect("params");
|
||||
assert_eq!(params["threadId"], "thread-plan-1");
|
||||
assert_eq!(params["turnId"], "turn-plan-1");
|
||||
assert_eq!(params["plan"][1]["status"], "inProgress");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_update_plan_status() {
|
||||
assert!(matches!(
|
||||
project(&event(json!({
|
||||
"plan": [{ "step": "补主链", "status": "running" }]
|
||||
}))),
|
||||
EventProjection::Reject(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use super::NoopAppDataSource;
|
||||
use super::RuntimeCoreError;
|
||||
use app_server_protocol::*;
|
||||
use async_trait::async_trait;
|
||||
use lime_mcp::McpRuntimeServerSpec;
|
||||
use lime_mcp::{McpOAuthLoginHandle, McpRuntimeServerSpec};
|
||||
|
||||
#[async_trait]
|
||||
pub trait McpAppDataSource: Send + Sync {
|
||||
@@ -79,7 +79,7 @@ pub trait McpAppDataSource: Send + Sync {
|
||||
async fn login_mcp_server_oauth(
|
||||
&self,
|
||||
_params: McpServerOauthLoginParams,
|
||||
) -> Result<McpServerOauthLoginResponse, RuntimeCoreError> {
|
||||
) -> Result<McpOAuthLoginHandle, RuntimeCoreError> {
|
||||
Err(requires_current("mcpServer/oauth/login"))
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ pub(in crate::runtime) async fn projection_load_context(
|
||||
.collect(),
|
||||
};
|
||||
let mut detail = if stored.events.is_empty() && params.history_limit.is_some() {
|
||||
let (mut usage_events, warning_events): (Vec<_>, Vec<_>) = projection_support_events
|
||||
let (mut usage_events, notification_events): (Vec<_>, Vec<_>) = projection_support_events
|
||||
.into_iter()
|
||||
.partition(is_turn_completed_usage_event);
|
||||
usage_events.extend(
|
||||
@@ -243,7 +243,7 @@ pub(in crate::runtime) async fn projection_load_context(
|
||||
&projection,
|
||||
params,
|
||||
&usage_events,
|
||||
&warning_events,
|
||||
¬ification_events,
|
||||
projection_store,
|
||||
)
|
||||
.await?
|
||||
@@ -288,7 +288,7 @@ async fn projection_summary_detail(
|
||||
projection: &ProjectionReadSession,
|
||||
params: &AgentSessionReadParams,
|
||||
usage_events: &[AgentEvent],
|
||||
warning_events: &[AgentEvent],
|
||||
notification_events: &[AgentEvent],
|
||||
projection_store: &super::ProjectionStore,
|
||||
) -> Result<serde_json::Value, RuntimeCoreError> {
|
||||
let messages = projection.messages.clone();
|
||||
@@ -300,10 +300,10 @@ async fn projection_summary_detail(
|
||||
let canonical_items = read_model::canonical_items_from_thread_store(projection_store, stored)
|
||||
.await
|
||||
.map_err(|error| RuntimeCoreError::Backend(error.to_string()))?;
|
||||
let mut warning_stored = stored.clone();
|
||||
warning_stored.events = warning_events.to_vec();
|
||||
let items = Value::Array(read_model::merge_runtime_warning_items(
|
||||
&warning_stored,
|
||||
let mut notification_stored = stored.clone();
|
||||
notification_stored.events = notification_events.to_vec();
|
||||
let items = Value::Array(read_model::merge_runtime_notification_items(
|
||||
¬ification_stored,
|
||||
canonical_items,
|
||||
));
|
||||
let thread_items = items.clone();
|
||||
|
||||
@@ -72,7 +72,7 @@ impl RuntimeCore {
|
||||
pub async fn login_mcp_server_oauth(
|
||||
&self,
|
||||
params: McpServerOauthLoginParams,
|
||||
) -> Result<McpServerOauthLoginResponse, RuntimeCoreError> {
|
||||
) -> Result<lime_mcp::McpOAuthLoginHandle, RuntimeCoreError> {
|
||||
self.app_data_source.login_mcp_server_oauth(params).await
|
||||
}
|
||||
|
||||
|
||||
@@ -48,13 +48,14 @@ use thread_store::{ListItemsParams, PageRequest, ThreadStore, ThreadStoreResult}
|
||||
pub(super) use canonical_items::canonical_item_to_agent_detail;
|
||||
pub(super) use messages::runtime_session_messages;
|
||||
|
||||
pub(super) fn merge_runtime_warning_items(
|
||||
pub(super) fn merge_runtime_notification_items(
|
||||
stored: &StoredSession,
|
||||
mut items: Vec<serde_json::Value>,
|
||||
) -> Vec<serde_json::Value> {
|
||||
// Warning notifications are durable runtime events, not canonical ThreadItem
|
||||
// variants. Re-derive them beside canonical items for every cold-read path.
|
||||
// Runtime notifications are durable events, not canonical ThreadItem variants.
|
||||
// Re-derive them beside canonical items for every cold-read path.
|
||||
items.extend(runtime_warning_items_from_events(stored));
|
||||
items.extend(runtime_error_items_from_events(stored));
|
||||
sort_read_detail_items(&mut items);
|
||||
items
|
||||
}
|
||||
@@ -151,7 +152,7 @@ fn runtime_session_read_detail_with_item_source(
|
||||
sort_read_detail_items(&mut items);
|
||||
items
|
||||
},
|
||||
|items| merge_runtime_warning_items(stored, items.to_vec()),
|
||||
|items| merge_runtime_notification_items(stored, items.to_vec()),
|
||||
);
|
||||
if canonical_items.is_some() {
|
||||
apply_canonical_item_views(&mut thread_read, &items);
|
||||
|
||||
@@ -85,6 +85,47 @@ fn canonical_read_keeps_runtime_warning_message_and_localization_code() {
|
||||
assert_eq!(detail["thread_read"]["thread_items"], detail["items"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_read_keeps_runtime_error_without_terminalizing_the_turn() {
|
||||
let mut stored = stored_running_session("2026-07-31T00:00:00Z", "2026-07-31T00:00:01Z");
|
||||
stored.events.push(AgentEvent {
|
||||
event_id: "event-read-model-error".to_string(),
|
||||
sequence: 2,
|
||||
session_id: stored.session.session_id.clone(),
|
||||
thread_id: Some(stored.session.thread_id.clone()),
|
||||
turn_id: Some(stored.turns[0].turn_id.clone()),
|
||||
event_type: "runtime.error".to_string(),
|
||||
timestamp: "2026-07-31T00:00:02Z".to_string(),
|
||||
payload: json!({
|
||||
"errorCode": "server_overloaded",
|
||||
"message": "服务暂时不可用,等待权威终态。",
|
||||
"willRetry": false
|
||||
}),
|
||||
});
|
||||
|
||||
let detail = runtime_session_read_detail_with_item_source(
|
||||
&stored,
|
||||
ReadDetailOptions::default(),
|
||||
&[],
|
||||
Some(&[]),
|
||||
);
|
||||
let error = detail["items"]
|
||||
.as_array()
|
||||
.and_then(|items| items.iter().find(|item| item["type"] == "error"))
|
||||
.expect("error item");
|
||||
|
||||
assert_eq!(error["thread_id"], stored.session.thread_id);
|
||||
assert_eq!(error["turn_id"], stored.turns[0].turn_id);
|
||||
assert_eq!(error["message"], "服务暂时不可用,等待权威终态。");
|
||||
assert_eq!(detail["turns"][0]["status"], "running");
|
||||
assert_eq!(
|
||||
detail["thread_read"]["diagnostics"]["latest_turn_status"],
|
||||
"running"
|
||||
);
|
||||
assert!(detail["turns"][0]["completed_at"].is_null());
|
||||
assert_eq!(detail["thread_read"]["thread_items"], detail["items"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_read_rejects_malformed_runtime_warning_shapes_and_identity() {
|
||||
let mut stored = stored_running_session("2026-07-31T00:00:00Z", "2026-07-31T00:00:01Z");
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::RuntimeEvent;
|
||||
use agent_protocol::ToolOutput;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
const UPDATE_PLAN_ACK: &str = "Plan updated";
|
||||
|
||||
pub fn plan_delta_event(text: impl Into<String>, revision_id: impl Into<String>) -> RuntimeEvent {
|
||||
let text = text.into();
|
||||
@@ -68,52 +66,31 @@ pub fn proposed_plan_final_event(
|
||||
event
|
||||
}
|
||||
|
||||
pub fn plan_final_event_from_update_plan_result(
|
||||
pub fn turn_plan_updated_event_from_update_plan_result(
|
||||
tool_id: &str,
|
||||
output: &str,
|
||||
metadata: Option<&HashMap<String, Value>>,
|
||||
output: &ToolOutput,
|
||||
) -> Option<RuntimeEvent> {
|
||||
if output.trim() != UPDATE_PLAN_ACK {
|
||||
if output.text.as_deref()?.trim() != tool_runtime::update_plan::PLAN_UPDATED_MESSAGE {
|
||||
return None;
|
||||
}
|
||||
let metadata = metadata?;
|
||||
let plan = metadata.get("plan")?.clone();
|
||||
let text = plan_text_from_plan_value(&plan)?;
|
||||
let revision_id = format!("update_plan:{tool_id}");
|
||||
let mut event = plan_final_event(text, revision_id, Some(plan));
|
||||
if let Some(payload_object) = event.payload.as_object_mut() {
|
||||
payload_object.insert("toolCallId".to_string(), Value::String(tool_id.to_string()));
|
||||
payload_object.insert(
|
||||
"source".to_string(),
|
||||
Value::String("update_plan".to_string()),
|
||||
);
|
||||
if let Some(explanation) = metadata.get("explanation") {
|
||||
if !explanation.is_null() {
|
||||
payload_object.insert("explanation".to_string(), explanation.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(event)
|
||||
}
|
||||
|
||||
fn plan_text_from_plan_value(plan: &Value) -> Option<String> {
|
||||
let lines = plan
|
||||
.as_array()?
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let step = item.get("step")?.as_str()?.trim();
|
||||
if step.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let status = item
|
||||
.get("status")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("pending");
|
||||
let marker = if status == "completed" { "[x]" } else { "[ ]" };
|
||||
Some(format!("- {marker} {step}"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(!lines.is_empty()).then(|| lines.join("\n"))
|
||||
let structured_content = output.structured_content.as_ref()?.as_object()?;
|
||||
let plan = serde_json::from_value::<Vec<tool_runtime::update_plan::PlanStep>>(
|
||||
structured_content.get("plan")?.clone(),
|
||||
)
|
||||
.ok()?;
|
||||
let explanation = match structured_content.get("explanation") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(explanation)) => Some(explanation.clone()),
|
||||
Some(_) => return None,
|
||||
};
|
||||
Some(RuntimeEvent::new(
|
||||
"turn.plan.updated",
|
||||
json!({
|
||||
"toolCallId": tool_id,
|
||||
"explanation": explanation,
|
||||
"plan": plan,
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
fn plan_value_from_markdown_text(text: &str) -> Option<Value> {
|
||||
@@ -190,29 +167,41 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_update_plan_final_event_from_tool_metadata() {
|
||||
let event = plan_final_event_from_update_plan_result(
|
||||
fn builds_turn_plan_updated_event_from_canonical_tool_output() {
|
||||
let event = turn_plan_updated_event_from_update_plan_result(
|
||||
"tool-plan-1",
|
||||
"Plan updated",
|
||||
Some(&HashMap::from([
|
||||
(
|
||||
"plan".to_string(),
|
||||
json!([
|
||||
&ToolOutput {
|
||||
text: Some("Plan updated".to_string()),
|
||||
structured_content: Some(json!({
|
||||
"tool_family": "update_plan",
|
||||
"explanation": "继续实现",
|
||||
"plan": [
|
||||
{ "step": "读现状", "status": "completed" },
|
||||
{ "step": "补主链", "status": "in_progress" }
|
||||
]),
|
||||
),
|
||||
("explanation".to_string(), json!("继续实现")),
|
||||
])),
|
||||
]
|
||||
})),
|
||||
..ToolOutput::default()
|
||||
},
|
||||
)
|
||||
.expect("update_plan result should become plan.final");
|
||||
.expect("update_plan result should become turn.plan.updated");
|
||||
|
||||
assert_eq!(event.event_type, "plan.final");
|
||||
assert_eq!(event.payload["revisionId"], "update_plan:tool-plan-1");
|
||||
assert_eq!(event.event_type, "turn.plan.updated");
|
||||
assert_eq!(event.payload["toolCallId"], "tool-plan-1");
|
||||
assert_eq!(event.payload["source"], "update_plan");
|
||||
assert_eq!(event.payload["explanation"], "继续实现");
|
||||
assert_eq!(event.payload["text"], "- [x] 读现状\n- [ ] 补主链");
|
||||
assert_eq!(event.payload["plan"][1]["status"], "in_progress");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_update_plan_ack_without_canonical_structured_output() {
|
||||
let event = turn_plan_updated_event_from_update_plan_result(
|
||||
"tool-plan-1",
|
||||
&ToolOutput {
|
||||
text: Some("Plan updated".to_string()),
|
||||
structured_content: None,
|
||||
..ToolOutput::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert!(event.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,19 +174,13 @@ fn update_plan_event_from_completed_tool(event: &RuntimeAgentEvent) -> Option<Ru
|
||||
};
|
||||
let tool = canonical_tool_item(event)?;
|
||||
let output = tool.output?;
|
||||
if !tool.item.status.is_terminal() || output.error.is_some() {
|
||||
if tool.name != tool_runtime::update_plan::UPDATE_PLAN_NAME
|
||||
|| !tool.item.status.is_terminal()
|
||||
|| output.error.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let metadata = tool
|
||||
.item
|
||||
.metadata
|
||||
.as_object()
|
||||
.map(|metadata| metadata.clone().into_iter().collect());
|
||||
plan_events::plan_final_event_from_update_plan_result(
|
||||
tool.call_id,
|
||||
output.text.as_deref().unwrap_or_default(),
|
||||
metadata.as_ref(),
|
||||
)
|
||||
plan_events::turn_plan_updated_event_from_update_plan_result(tool.call_id, output)
|
||||
}
|
||||
|
||||
fn runtime_event_type_for_agent_event(event: &RuntimeAgentEvent, raw_type: &str) -> &'static str {
|
||||
@@ -208,6 +202,7 @@ fn provider_stream_runtime_event_type(runtime_event_kind: &str) -> &'static str
|
||||
struct CanonicalToolItem<'a> {
|
||||
item: &'a ThreadItem,
|
||||
call_id: &'a str,
|
||||
name: &'a str,
|
||||
output: Option<&'a ToolOutput>,
|
||||
}
|
||||
|
||||
@@ -219,7 +214,10 @@ fn canonical_tool_item(event: &RuntimeAgentEvent) -> Option<CanonicalToolItem<'_
|
||||
_ => return None,
|
||||
};
|
||||
let ThreadItemPayload::Tool {
|
||||
call_id, output, ..
|
||||
call_id,
|
||||
name,
|
||||
output,
|
||||
..
|
||||
} = &item.payload
|
||||
else {
|
||||
return None;
|
||||
@@ -227,6 +225,7 @@ fn canonical_tool_item(event: &RuntimeAgentEvent) -> Option<CanonicalToolItem<'_
|
||||
Some(CanonicalToolItem {
|
||||
item,
|
||||
call_id,
|
||||
name,
|
||||
output: output.as_ref(),
|
||||
})
|
||||
}
|
||||
@@ -717,7 +716,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_update_plan_completion_emits_plan_final_fact() {
|
||||
fn canonical_update_plan_completion_emits_turn_plan_updated_fact() {
|
||||
let events = runtime_events_from_agent_event(&canonical_tool_event(
|
||||
"tool-plan",
|
||||
"update_plan",
|
||||
@@ -726,18 +725,16 @@ mod tests {
|
||||
success: true,
|
||||
output: "Plan updated".to_string(),
|
||||
error: None,
|
||||
structured_content: None,
|
||||
structured_content: Some(json!({
|
||||
"tool_family": "update_plan",
|
||||
"explanation": "开始实现",
|
||||
"plan": [
|
||||
{ "step": "读现状", "status": "completed" },
|
||||
{ "step": "打通主链", "status": "in_progress" }
|
||||
]
|
||||
})),
|
||||
images: None,
|
||||
metadata: Some(HashMap::from([
|
||||
(
|
||||
"plan".to_string(),
|
||||
json!([
|
||||
{ "step": "读现状", "status": "completed" },
|
||||
{ "step": "打通主链", "status": "in_progress" }
|
||||
]),
|
||||
),
|
||||
("explanation".to_string(), json!("开始实现")),
|
||||
])),
|
||||
metadata: None,
|
||||
}),
|
||||
))
|
||||
.expect("update_plan item completion should emit");
|
||||
@@ -747,9 +744,34 @@ mod tests {
|
||||
.iter()
|
||||
.map(|event| event.event_type.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["item.completed", "plan.final"]
|
||||
vec!["item.completed", "turn.plan.updated"]
|
||||
);
|
||||
assert_eq!(events[1].payload["revisionId"], "update_plan:tool-plan");
|
||||
assert_eq!(events[1].payload["toolCallId"], "tool-plan");
|
||||
assert_eq!(events[1].payload["explanation"], "开始实现");
|
||||
assert_eq!(events[1].payload["plan"][1]["status"], "in_progress");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_update_plan_completion_does_not_emit_turn_plan_updated_fact() {
|
||||
let events = runtime_events_from_agent_event(&canonical_tool_event(
|
||||
"tool-other",
|
||||
"other_tool",
|
||||
Some(json!({})),
|
||||
Some(AgentToolResult {
|
||||
success: true,
|
||||
output: "Plan updated".to_string(),
|
||||
error: None,
|
||||
structured_content: Some(json!({
|
||||
"plan": [{ "step": "不应投影", "status": "completed" }]
|
||||
})),
|
||||
images: None,
|
||||
metadata: None,
|
||||
}),
|
||||
))
|
||||
.expect("other tool item completion should emit");
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0].event_type, "item.completed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
use crate::{AppServerEventBridge, JsonRpcMessage};
|
||||
use app_server_protocol::protocol::v2::{ServerNotification, SkillsChangedNotification};
|
||||
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const WATCHER_THROTTLE_INTERVAL: Duration = Duration::from_secs(10);
|
||||
|
||||
pub(crate) struct SkillsWatcher {
|
||||
_watcher: Arc<Mutex<RecommendedWatcher>>,
|
||||
_watched_roots: Arc<Mutex<HashSet<PathBuf>>>,
|
||||
reconcile_task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl Drop for SkillsWatcher {
|
||||
fn drop(&mut self) {
|
||||
self.reconcile_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl SkillsWatcher {
|
||||
pub(crate) fn start_default(bridge: AppServerEventBridge) -> Option<Arc<Self>> {
|
||||
Self::start(
|
||||
bridge,
|
||||
lime_skills::default_agent_skill_roots()
|
||||
.into_iter()
|
||||
.map(|root| root.path),
|
||||
WATCHER_THROTTLE_INTERVAL,
|
||||
)
|
||||
}
|
||||
|
||||
fn start(
|
||||
bridge: AppServerEventBridge,
|
||||
roots: impl IntoIterator<Item = PathBuf>,
|
||||
throttle_interval: Duration,
|
||||
) -> Option<Arc<Self>> {
|
||||
let handle = match tokio::runtime::Handle::try_current() {
|
||||
Ok(handle) => handle,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "skills watcher skipped without Tokio runtime");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let roots = roots.into_iter().collect::<HashSet<_>>();
|
||||
if roots.is_empty() {
|
||||
tracing::warn!("skills watcher has no default Skill roots");
|
||||
return None;
|
||||
}
|
||||
let last_notification = Arc::new(Mutex::new(None::<Instant>));
|
||||
let callback_last_notification = last_notification.clone();
|
||||
let callback_bridge = bridge.clone();
|
||||
let callback_handle = handle.clone();
|
||||
let watcher = match notify::recommended_watcher(move |result: notify::Result<Event>| {
|
||||
let event = match result {
|
||||
Ok(event) if is_catalog_change(&event.kind) => event,
|
||||
Ok(_) => return,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "skills watcher received an error");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if event.paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
notify_catalog_changed(
|
||||
&callback_bridge,
|
||||
&callback_handle,
|
||||
&callback_last_notification,
|
||||
throttle_interval,
|
||||
);
|
||||
}) {
|
||||
Ok(watcher) => watcher,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "failed to initialize skills watcher");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let watcher = Arc::new(Mutex::new(watcher));
|
||||
let watched_roots = Arc::new(Mutex::new(HashSet::new()));
|
||||
reconcile_watches(&watcher, &watched_roots, &roots);
|
||||
|
||||
let reconcile_watcher = watcher.clone();
|
||||
let reconcile_watched_roots = watched_roots.clone();
|
||||
let reconcile_last_notification = last_notification.clone();
|
||||
let reconcile_handle = handle.clone();
|
||||
let reconcile_task = handle.spawn(async move {
|
||||
let mut interval = tokio::time::interval(throttle_interval);
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
interval.tick().await;
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if reconcile_watches(&reconcile_watcher, &reconcile_watched_roots, &roots) {
|
||||
notify_catalog_changed(
|
||||
&bridge,
|
||||
&reconcile_handle,
|
||||
&reconcile_last_notification,
|
||||
throttle_interval,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Some(Arc::new(Self {
|
||||
_watcher: watcher,
|
||||
_watched_roots: watched_roots,
|
||||
reconcile_task,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn reconcile_watches(
|
||||
watcher: &Mutex<RecommendedWatcher>,
|
||||
watched_roots: &Mutex<HashSet<PathBuf>>,
|
||||
roots: &HashSet<PathBuf>,
|
||||
) -> bool {
|
||||
let existing_roots = roots
|
||||
.iter()
|
||||
.filter(|root| root.is_dir())
|
||||
.map(|root| std::fs::canonicalize(root).unwrap_or_else(|_| root.clone()))
|
||||
.collect::<HashSet<_>>();
|
||||
let mut watcher = watcher.lock().unwrap_or_else(|error| error.into_inner());
|
||||
let mut watched_roots = watched_roots
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
let mut changed = false;
|
||||
|
||||
for removed in watched_roots
|
||||
.difference(&existing_roots)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
if let Err(error) = watcher.unwatch(&removed) {
|
||||
tracing::debug!(path = %removed.display(), %error, "Skill root was already unwatched");
|
||||
}
|
||||
watched_roots.remove(&removed);
|
||||
changed = true;
|
||||
}
|
||||
for added in existing_roots
|
||||
.difference(&watched_roots)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
match watcher.watch(&added, RecursiveMode::Recursive) {
|
||||
Ok(()) => {
|
||||
watched_roots.insert(added);
|
||||
changed = true;
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(path = %added.display(), %error, "failed to watch Skill root");
|
||||
}
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn notify_catalog_changed(
|
||||
bridge: &AppServerEventBridge,
|
||||
handle: &tokio::runtime::Handle,
|
||||
last_notification: &Mutex<Option<Instant>>,
|
||||
throttle_interval: Duration,
|
||||
) {
|
||||
let now = Instant::now();
|
||||
let mut last = last_notification
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
if last.is_some_and(|last| now.duration_since(last) < throttle_interval) {
|
||||
return;
|
||||
}
|
||||
*last = Some(now);
|
||||
drop(last);
|
||||
|
||||
lime_skills::invalidate_agent_skill_snapshot_cache();
|
||||
let bridge = bridge.clone();
|
||||
handle.spawn(async move {
|
||||
bridge
|
||||
.broadcast_message(JsonRpcMessage::Notification(
|
||||
ServerNotification::SkillsChanged(SkillsChangedNotification {}).into(),
|
||||
))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
fn is_catalog_change(kind: &EventKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AppServer;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn only_catalog_mutations_trigger_invalidation() {
|
||||
assert!(is_catalog_change(&EventKind::Create(
|
||||
notify::event::CreateKind::File
|
||||
)));
|
||||
assert!(is_catalog_change(&EventKind::Modify(
|
||||
notify::event::ModifyKind::Data(notify::event::DataChange::Content)
|
||||
)));
|
||||
assert!(is_catalog_change(&EventKind::Remove(
|
||||
notify::event::RemoveKind::Folder
|
||||
)));
|
||||
assert!(!is_catalog_change(&EventKind::Access(
|
||||
notify::event::AccessKind::Read
|
||||
)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn watched_skill_file_change_broadcasts_typed_invalidation() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let root = temp.path().join("skills");
|
||||
let skill_dir = root.join("writer");
|
||||
fs::create_dir_all(&skill_dir).expect("create Skill directory");
|
||||
let skill_file = skill_dir.join("SKILL.md");
|
||||
fs::write(&skill_file, "---\nname: writer\ndescription: before\n---\n")
|
||||
.expect("write Skill fixture");
|
||||
|
||||
let server = AppServer::new();
|
||||
let mut outbound = server.subscribe_outbound_messages();
|
||||
let _watcher =
|
||||
SkillsWatcher::start(server.event_bridge(), [root], Duration::from_millis(20))
|
||||
.expect("start Skill watcher");
|
||||
fs::write(&skill_file, "---\nname: writer\ndescription: after\n---\n")
|
||||
.expect("update Skill fixture");
|
||||
|
||||
let notification = tokio::time::timeout(Duration::from_secs(3), async {
|
||||
loop {
|
||||
let message = outbound.recv().await.expect("watcher notification");
|
||||
if matches!(
|
||||
&message,
|
||||
JsonRpcMessage::Notification(notification)
|
||||
if notification.method == "skills/changed"
|
||||
) {
|
||||
break message;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("wait for skills/changed");
|
||||
assert_eq!(
|
||||
notification,
|
||||
JsonRpcMessage::Notification(
|
||||
ServerNotification::SkillsChanged(SkillsChangedNotification {}).into()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skill_root_created_after_start_broadcasts_typed_invalidation() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let root = temp.path().join("skills");
|
||||
let server = AppServer::new();
|
||||
let mut outbound = server.subscribe_outbound_messages();
|
||||
let _watcher = SkillsWatcher::start(
|
||||
server.event_bridge(),
|
||||
[root.clone()],
|
||||
Duration::from_millis(20),
|
||||
)
|
||||
.expect("start Skill watcher before root exists");
|
||||
|
||||
let skill_dir = root.join("writer");
|
||||
fs::create_dir_all(&skill_dir).expect("create Skill root and directory");
|
||||
fs::write(
|
||||
skill_dir.join("SKILL.md"),
|
||||
"---\nname: writer\ndescription: created later\n---\n",
|
||||
)
|
||||
.expect("write Skill fixture");
|
||||
|
||||
let notification = tokio::time::timeout(Duration::from_secs(3), async {
|
||||
loop {
|
||||
let message = outbound.recv().await.expect("watcher notification");
|
||||
if matches!(
|
||||
&message,
|
||||
JsonRpcMessage::Notification(notification)
|
||||
if notification.method == "skills/changed"
|
||||
) {
|
||||
break message;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("wait for root creation skills/changed");
|
||||
assert_eq!(
|
||||
notification,
|
||||
JsonRpcMessage::Notification(
|
||||
ServerNotification::SkillsChanged(SkillsChangedNotification {}).into()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,3 @@ pub struct McpResourceUpdatedPayload {
|
||||
pub server_name: String,
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
/// OAuth 授权完成事件
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct McpOAuthCompletedPayload {
|
||||
pub server_name: String,
|
||||
}
|
||||
|
||||
@@ -31,13 +31,12 @@ pub use elicitation::{
|
||||
ElicitationRequestRouter, ElicitationResponse, ElicitationRouterError,
|
||||
};
|
||||
pub use events::{
|
||||
McpOAuthCompletedPayload, McpResourceUpdatedPayload, McpResourcesUpdatedPayload,
|
||||
McpServerErrorPayload, McpServerStartedPayload, McpServerStoppedPayload,
|
||||
McpToolsUpdatedPayload,
|
||||
McpResourceUpdatedPayload, McpResourcesUpdatedPayload, McpServerErrorPayload,
|
||||
McpServerStartedPayload, McpServerStoppedPayload, McpToolsUpdatedPayload,
|
||||
};
|
||||
pub use extension_surface::{build_runtime_extension_surface, runtime_extension_name};
|
||||
pub use manager::{McpBridgeSnapshot, McpClientManager, McpRuntimeOwner, McpRuntimeServerSpec};
|
||||
pub use oauth::{McpOAuthLoginParams, McpOAuthLoginResponse, McpOAuthRegistry};
|
||||
pub use oauth::{McpOAuthLoginHandle, McpOAuthLoginParams, McpOAuthRegistry};
|
||||
pub use tool_converter::ToolConverter;
|
||||
pub use types::{
|
||||
McpContent, McpError, McpManagerState, McpPromptArgument, McpPromptDefinition,
|
||||
|
||||
@@ -38,7 +38,7 @@ use tracing::{debug, info, warn};
|
||||
use crate::client::McpClientWrapper;
|
||||
use crate::environment::McpEnvironmentRegistry;
|
||||
use crate::events::*;
|
||||
use crate::oauth::{McpOAuthLoginResponse, McpOAuthRegistry};
|
||||
use crate::oauth::{McpOAuthLoginHandle, McpOAuthRegistry};
|
||||
use crate::types::*;
|
||||
|
||||
mod lifecycle;
|
||||
@@ -373,9 +373,9 @@ impl McpClientManager {
|
||||
config: &McpServerConfig,
|
||||
scopes: Option<Vec<String>>,
|
||||
timeout_secs: Option<u64>,
|
||||
) -> Result<McpOAuthLoginResponse, McpError> {
|
||||
) -> Result<McpOAuthLoginHandle, McpError> {
|
||||
self.oauth_registry
|
||||
.start_login(name, config, scopes, timeout_secs, self.emitter.clone())
|
||||
.start_login(name, config, scopes, timeout_secs)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::events::{McpOAuthCompletedPayload, McpServerErrorPayload};
|
||||
use crate::oauth_store::PersistentCredentialStore;
|
||||
use crate::streamable_http::build_default_headers;
|
||||
use crate::types::{McpError, McpServerConfig, McpServerTransport};
|
||||
@@ -7,11 +6,12 @@ use axum::http::StatusCode;
|
||||
use axum::response::{Html, IntoResponse};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use lime_core::DynEmitter;
|
||||
use rmcp::transport::auth::{AuthorizationManager, CredentialStore, OAuthState, StoredCredentials};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::future::Future;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::{oneshot, Mutex};
|
||||
@@ -28,11 +28,38 @@ pub struct McpOAuthLoginParams {
|
||||
pub timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct McpOAuthLoginResponse {
|
||||
pub struct McpOAuthLoginHandle {
|
||||
pub authorization_url: String,
|
||||
pub state: String,
|
||||
completion: Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + 'static>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for McpOAuthLoginHandle {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter
|
||||
.debug_struct("McpOAuthLoginHandle")
|
||||
.field("authorization_url", &self.authorization_url)
|
||||
.field("state", &self.state)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl McpOAuthLoginHandle {
|
||||
pub fn new(
|
||||
authorization_url: impl Into<String>,
|
||||
state: impl Into<String>,
|
||||
completion: impl Future<Output = Result<(), McpError>> + Send + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
authorization_url: authorization_url.into(),
|
||||
state: state.into(),
|
||||
completion: Box::pin(completion),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> Result<(), McpError> {
|
||||
self.completion.await
|
||||
}
|
||||
}
|
||||
|
||||
/// OAuth 凭据根只能由组合根注入。未注入时所有凭据操作 fail closed,
|
||||
@@ -59,8 +86,7 @@ impl McpOAuthRegistry {
|
||||
config: &McpServerConfig,
|
||||
scopes: Option<Vec<String>>,
|
||||
timeout_secs: Option<u64>,
|
||||
emitter: Option<DynEmitter>,
|
||||
) -> Result<McpOAuthLoginResponse, McpError> {
|
||||
) -> Result<McpOAuthLoginHandle, McpError> {
|
||||
let (url, bearer_token_env_var, http_headers, env_http_headers) = match &config.transport {
|
||||
McpServerTransport::StreamableHttp {
|
||||
url,
|
||||
@@ -133,36 +159,30 @@ impl McpOAuthRegistry {
|
||||
let wait_secs = timeout_secs
|
||||
.unwrap_or(DEFAULT_OAUTH_TIMEOUT_SECS)
|
||||
.clamp(30, 900);
|
||||
let server_name_for_error = server_name.clone();
|
||||
let emitter_for_error = emitter.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = complete_oauth_login(
|
||||
let completion = async move {
|
||||
let result = complete_oauth_login(
|
||||
auth_state,
|
||||
store,
|
||||
callback_rx,
|
||||
wait_secs,
|
||||
server_name.clone(),
|
||||
emitter,
|
||||
)
|
||||
.await
|
||||
{
|
||||
.await;
|
||||
if let Err(error) = &result {
|
||||
tracing::warn!(
|
||||
server_name = %server_name,
|
||||
error = %error,
|
||||
"MCP OAuth login did not complete"
|
||||
);
|
||||
emit_oauth_error(
|
||||
emitter_for_error,
|
||||
&server_name_for_error,
|
||||
&error.to_string(),
|
||||
);
|
||||
}
|
||||
});
|
||||
result
|
||||
};
|
||||
|
||||
Ok(McpOAuthLoginResponse {
|
||||
Ok(McpOAuthLoginHandle::new(
|
||||
authorization_url,
|
||||
state: "pending".to_string(),
|
||||
})
|
||||
"pending",
|
||||
completion,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn authorized_manager_for(
|
||||
@@ -360,7 +380,6 @@ async fn complete_oauth_login(
|
||||
callback_rx: oneshot::Receiver<OAuthCallbackResult>,
|
||||
timeout_secs: u64,
|
||||
server_name: String,
|
||||
emitter: Option<DynEmitter>,
|
||||
) -> Result<(), McpError> {
|
||||
let login_timeout = Duration::from_secs(timeout_secs);
|
||||
let started_at = Instant::now();
|
||||
@@ -410,49 +429,9 @@ async fn complete_oauth_login(
|
||||
})
|
||||
.await
|
||||
.map_err(|_| McpError::Timeout)??;
|
||||
emit_oauth_completed(emitter, &server_name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_oauth_completed(emitter: Option<DynEmitter>, server_name: &str) {
|
||||
let Some(emitter) = emitter else {
|
||||
return;
|
||||
};
|
||||
let payload = McpOAuthCompletedPayload {
|
||||
server_name: server_name.to_string(),
|
||||
};
|
||||
let Ok(value) = serde_json::to_value(&payload) else {
|
||||
return;
|
||||
};
|
||||
if let Err(error) = emitter.emit_event("mcp:oauth_completed", &value) {
|
||||
tracing::warn!(
|
||||
server_name = %server_name,
|
||||
error = %error,
|
||||
"MCP OAuth completion event failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_oauth_error(emitter: Option<DynEmitter>, server_name: &str, error: &str) {
|
||||
let Some(emitter) = emitter else {
|
||||
return;
|
||||
};
|
||||
let payload = McpServerErrorPayload {
|
||||
server_name: server_name.to_string(),
|
||||
error: error.to_string(),
|
||||
};
|
||||
let Ok(value) = serde_json::to_value(&payload) else {
|
||||
return;
|
||||
};
|
||||
if let Err(error) = emitter.emit_event("mcp:server_error", &value) {
|
||||
tracing::warn!(
|
||||
server_name = %server_name,
|
||||
error = %error,
|
||||
"MCP OAuth error event failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn discover_supported_scopes(
|
||||
url: &str,
|
||||
client: reqwest::Client,
|
||||
|
||||
@@ -6,28 +6,11 @@ use axum::http::{header::LOCATION, HeaderMap, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use lime_core::EventEmit;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct RecordingEmitter {
|
||||
events: Arc<Mutex<Vec<(String, Value)>>>,
|
||||
}
|
||||
|
||||
impl EventEmit for RecordingEmitter {
|
||||
fn emit_event(&self, event: &str, payload: &Value) -> Result<(), String> {
|
||||
self.events
|
||||
.lock()
|
||||
.expect("events lock")
|
||||
.push((event.to_string(), payload.clone()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn oauth_config_with_explicit_client_id() -> McpServerConfig {
|
||||
McpServerConfig {
|
||||
transport: McpServerTransport::StreamableHttp {
|
||||
@@ -143,28 +126,6 @@ fn require_test_header(state: &TestOAuthProviderState, headers: &HeaderMap) {
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
async fn wait_for_event(
|
||||
events: Arc<Mutex<Vec<(String, Value)>>>,
|
||||
event_name: &str,
|
||||
server_name: &str,
|
||||
) {
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
let has_event = events
|
||||
.lock()
|
||||
.expect("events lock")
|
||||
.iter()
|
||||
.any(|(name, payload)| name == event_name && payload["server_name"] == server_name);
|
||||
if has_event {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("event should be emitted");
|
||||
}
|
||||
|
||||
fn oauth_browser_client() -> reqwest::Client {
|
||||
reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
@@ -300,37 +261,6 @@ fn merge_loopback_no_proxy_hosts_returns_none_when_complete() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_oauth_completed_sends_server_event() {
|
||||
let emitter = RecordingEmitter::default();
|
||||
let events = emitter.events.clone();
|
||||
|
||||
emit_oauth_completed(Some(DynEmitter::new(emitter)), "remote-docs");
|
||||
|
||||
let events = events.lock().expect("events lock");
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0].0, "mcp:oauth_completed");
|
||||
assert_eq!(events[0].1["server_name"], "remote-docs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_oauth_error_sends_server_error_event() {
|
||||
let emitter = RecordingEmitter::default();
|
||||
let events = emitter.events.clone();
|
||||
|
||||
emit_oauth_error(
|
||||
Some(DynEmitter::new(emitter)),
|
||||
"remote-docs",
|
||||
"invalid_scope",
|
||||
);
|
||||
|
||||
let events = events.lock().expect("events lock");
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_eq!(events[0].0, "mcp:server_error");
|
||||
assert_eq!(events[0].1["server_name"], "remote-docs");
|
||||
assert_eq!(events[0].1["error"], "invalid_scope");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn credential_lookup_fails_closed_without_injected_store_root() {
|
||||
let registry = McpOAuthRegistry::new();
|
||||
@@ -355,22 +285,13 @@ async fn start_login_completes_against_local_oauth_provider_and_persists_token()
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store_root = temp.path().join("oauth-store");
|
||||
let registry = McpOAuthRegistry::new_in(&store_root);
|
||||
let emitter = RecordingEmitter::default();
|
||||
let events = emitter.events.clone();
|
||||
|
||||
let login = registry
|
||||
.start_login(
|
||||
"remote-docs",
|
||||
&config,
|
||||
None,
|
||||
Some(30),
|
||||
Some(DynEmitter::new(emitter)),
|
||||
)
|
||||
.start_login("remote-docs", &config, None, Some(30))
|
||||
.await
|
||||
.expect("start OAuth login");
|
||||
|
||||
let response = oauth_browser_client()
|
||||
.get(login.authorization_url)
|
||||
.get(login.authorization_url.clone())
|
||||
.send()
|
||||
.await
|
||||
.expect("visit authorization URL");
|
||||
@@ -379,29 +300,12 @@ async fn start_login_completes_against_local_oauth_provider_and_persists_token()
|
||||
"callback page should be successful"
|
||||
);
|
||||
|
||||
let store = PersistentCredentialStore::new_in(&store_root, "remote-docs", &server_url);
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
let has_credentials = store
|
||||
.has_credentials()
|
||||
.await
|
||||
.expect("credential store should be readable");
|
||||
let has_event = events
|
||||
.lock()
|
||||
.expect("events lock")
|
||||
.iter()
|
||||
.any(|(name, payload)| {
|
||||
name == "mcp:oauth_completed" && payload["server_name"] == "remote-docs"
|
||||
});
|
||||
if has_credentials && has_event {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("OAuth completion should persist token and emit event");
|
||||
tokio::time::timeout(Duration::from_secs(5), login.wait())
|
||||
.await
|
||||
.expect("OAuth completion should finish")
|
||||
.expect("OAuth completion should succeed");
|
||||
|
||||
let store = PersistentCredentialStore::new_in(&store_root, "remote-docs", &server_url);
|
||||
let credentials = store
|
||||
.load()
|
||||
.await
|
||||
@@ -427,12 +331,12 @@ async fn start_login_uses_discovered_scopes_when_config_has_no_scopes() {
|
||||
let registry = McpOAuthRegistry::new_in(temp.path().join("oauth-store"));
|
||||
|
||||
let login = registry
|
||||
.start_login("remote-docs", &config, None, Some(30), None)
|
||||
.start_login("remote-docs", &config, None, Some(30))
|
||||
.await
|
||||
.expect("start OAuth login");
|
||||
|
||||
let response = oauth_browser_client()
|
||||
.get(login.authorization_url)
|
||||
.get(login.authorization_url.clone())
|
||||
.send()
|
||||
.await
|
||||
.expect("visit authorization URL");
|
||||
@@ -450,6 +354,7 @@ async fn start_login_uses_discovered_scopes_when_config_has_no_scopes() {
|
||||
.and_then(|query| query.get("scope"))
|
||||
.expect("authorize URL should include discovered scopes");
|
||||
assert_eq!(scope, "search.read search.write");
|
||||
login.wait().await.expect("OAuth completion should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -478,12 +383,12 @@ async fn start_login_applies_configured_headers_to_oauth_requests() {
|
||||
let registry = McpOAuthRegistry::new_in(&store_root);
|
||||
|
||||
let login = registry
|
||||
.start_login("remote-docs", &config, None, Some(30), None)
|
||||
.start_login("remote-docs", &config, None, Some(30))
|
||||
.await
|
||||
.expect("start OAuth login");
|
||||
|
||||
let response = oauth_browser_client()
|
||||
.get(login.authorization_url)
|
||||
.get(login.authorization_url.clone())
|
||||
.send()
|
||||
.await
|
||||
.expect("visit authorization URL");
|
||||
@@ -492,41 +397,27 @@ async fn start_login_applies_configured_headers_to_oauth_requests() {
|
||||
"callback page should be successful"
|
||||
);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(5), login.wait())
|
||||
.await
|
||||
.expect("OAuth completion should finish")
|
||||
.expect("OAuth completion should succeed");
|
||||
|
||||
let store = PersistentCredentialStore::new_in(&store_root, "remote-docs", &server_url);
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
if store
|
||||
.has_credentials()
|
||||
.await
|
||||
.expect("credential store should be readable")
|
||||
{
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("OAuth completion should persist token");
|
||||
assert!(store
|
||||
.has_credentials()
|
||||
.await
|
||||
.expect("credential store should be readable"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn oauth_callback_provider_error_emits_server_error_without_timeout() {
|
||||
async fn oauth_callback_provider_error_returns_without_timeout() {
|
||||
let provider = spawn_test_oauth_provider().await;
|
||||
let server_url = format!("{}/mcp", provider.base_url);
|
||||
let config = dynamic_oauth_config(server_url);
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let registry = McpOAuthRegistry::new_in(temp.path().join("oauth-store"));
|
||||
let emitter = RecordingEmitter::default();
|
||||
let events = emitter.events.clone();
|
||||
|
||||
let login = registry
|
||||
.start_login(
|
||||
"remote-docs",
|
||||
&config,
|
||||
None,
|
||||
Some(30),
|
||||
Some(DynEmitter::new(emitter)),
|
||||
)
|
||||
.start_login("remote-docs", &config, None, Some(30))
|
||||
.await
|
||||
.expect("start OAuth login");
|
||||
let mut callback_url =
|
||||
@@ -559,13 +450,10 @@ async fn oauth_callback_provider_error_emits_server_error_without_timeout() {
|
||||
.expect("visit callback URL");
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
wait_for_event(events.clone(), "mcp:server_error", "remote-docs").await;
|
||||
let events = events.lock().expect("events lock");
|
||||
let error = events
|
||||
.iter()
|
||||
.find(|(name, _)| name == "mcp:server_error")
|
||||
.map(|(_, payload)| payload["error"].as_str().unwrap_or_default())
|
||||
.expect("server error should be emitted");
|
||||
assert!(error.contains("invalid_scope"));
|
||||
assert!(error.contains("scope rejected"));
|
||||
let error = tokio::time::timeout(Duration::from_secs(5), login.wait())
|
||||
.await
|
||||
.expect("OAuth failure should return without waiting for timeout")
|
||||
.expect_err("provider error should fail OAuth completion");
|
||||
assert!(error.to_string().contains("invalid_scope"));
|
||||
assert!(error.to_string().contains("scope rejected"));
|
||||
}
|
||||
|
||||
@@ -210,6 +210,13 @@ fn snapshot_cache() -> &'static Mutex<Option<SnapshotCacheEntry>> {
|
||||
CACHE.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
pub fn invalidate_agent_skill_snapshot_cache() {
|
||||
let mut guard = snapshot_cache()
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
*guard = None;
|
||||
}
|
||||
|
||||
fn cached_snapshot(key: &str) -> Option<AgentSkillSnapshot> {
|
||||
let guard = snapshot_cache()
|
||||
.lock()
|
||||
|
||||
@@ -47,9 +47,10 @@ pub use agent_selection::{
|
||||
pub use agent_snapshot::{
|
||||
agent_skill_roots_for_workspace, build_agent_skill_snapshot,
|
||||
build_agent_skill_snapshot_from_roots, build_agent_skill_snapshot_from_workspace,
|
||||
default_agent_skill_roots, AgentSkillAuthority, AgentSkillDependencies, AgentSkillInterface,
|
||||
AgentSkillMetadata, AgentSkillPolicy, AgentSkillRoot, AgentSkillScope, AgentSkillSnapshot,
|
||||
AgentSkillSnapshotOptions, AgentSkillSource, AgentSkillToolDependency,
|
||||
default_agent_skill_roots, invalidate_agent_skill_snapshot_cache, AgentSkillAuthority,
|
||||
AgentSkillDependencies, AgentSkillInterface, AgentSkillMetadata, AgentSkillPolicy,
|
||||
AgentSkillRoot, AgentSkillScope, AgentSkillSnapshot, AgentSkillSnapshotOptions,
|
||||
AgentSkillSource, AgentSkillToolDependency,
|
||||
};
|
||||
pub use execution_callback::{
|
||||
events, ExecutionCallback, ExecutionCompletePayload, StepCompletePayload, StepErrorPayload,
|
||||
|
||||
+2
-1
@@ -5,7 +5,7 @@
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "1.117.0",
|
||||
"version": "1.118.0",
|
||||
"description": "AI content workspace for Chinese creators: desktop writing, research, prompt management, knowledge base, and multi-model workflows.",
|
||||
"keywords": [
|
||||
"ai",
|
||||
@@ -220,6 +220,7 @@
|
||||
"smoke:local-history-import-visual-audit": "node scripts/electron/local-history-import-visual-audit-smoke.mjs",
|
||||
"smoke:local-history-import-real-sample-visual-audit": "node scripts/electron/local-history-import-real-sample-visual-audit-smoke.mjs",
|
||||
"smoke:mcp-config-electron-fixture": "node scripts/electron/mcp-config-fixture-smoke.mjs",
|
||||
"smoke:mcp-oauth-notification-electron-fixture": "npm run build:renderer:electron && node scripts/electron/mcp-oauth-notification-fixture-smoke.mjs",
|
||||
"smoke:settings-about-electron-fixture": "node scripts/electron/settings-about-fixture-smoke.mjs",
|
||||
"smoke:settings-stats-electron-fixture": "node scripts/electron/settings-stats-fixture-smoke.mjs",
|
||||
"smoke:settings-environment-electron-fixture": "node scripts/electron/settings-environment-fixture-smoke.mjs",
|
||||
|
||||
@@ -86,6 +86,7 @@ const thread = await runtime.readThread({
|
||||
| `readThread(params, options?)` | `thread/read` | Thread read model | 供 projection hydration / repair 使用,建议 `includeTurns: true`。 |
|
||||
| `exportEvidence(params, options?)` | `evidence/export` | evidence export response | 缺 surface 时 fail closed,不伪造空 evidence。 |
|
||||
| `subscribeLifecycleEvents(listener)` | direct v2 notifications | unsubscribe handle | 分发 Thread / Turn / Item lifecycle 与 message delta。 |
|
||||
| `subscribeSignalEvents(listener)` | direct v2 `error` | unsubscribe handle | 分发 retry/terminal error signal,不进入 lifecycle verifier。 |
|
||||
| `dispatchEvent(message)` | local event router | boolean | 用于现有 gateway 把 JSON-RPC notification 喂给 client。 |
|
||||
| `nextEvent(timeoutMs?)` | gateway event source | notification | 优先 gateway `nextEvent`,其次 `drainEvents`。 |
|
||||
|
||||
@@ -108,7 +109,7 @@ client 只传递 lifecycle intent 和 facts,不维护 tool 状态机、subagen
|
||||
|
||||
## Browser-Safe Session Gateway
|
||||
|
||||
Renderer 宿主如果已经有自己的 App Server gateway,应从 browser-safe 子路径导入 session gateway 适配器,避免把根入口中的 Node 侧 sidecar / stdio client 打进前端包。该适配器仍返回标准 `AgentRuntimeClient`,覆盖 turn lifecycle、`readThread`、`exportEvidence`、`subscribeLifecycleEvents`、`dispatchEvent` 和 `nextEvent`;宿主缺少 evidence 或 event source 时会 fail closed,不会静默回退 mock。
|
||||
Renderer 宿主如果已经有自己的 App Server gateway,应从 browser-safe 子路径导入 session gateway 适配器,避免把根入口中的 Node 侧 sidecar / stdio client 打进前端包。该适配器仍返回标准 `AgentRuntimeClient`,覆盖 turn lifecycle、`readThread`、`exportEvidence`、`subscribeLifecycleEvents`、`subscribeSignalEvents`、`dispatchEvent` 和 `nextEvent`;宿主缺少 evidence 或 event source 时会 fail closed,不会静默回退 mock。
|
||||
|
||||
`createAgentRuntimeClientFromSessionGateway(...)` 只适配现有 session gateway,
|
||||
不会创建第二套 transport、lifecycle state 或兼容协议。
|
||||
@@ -154,7 +155,7 @@ const runtime: AgentRuntimeLifecycleClient =
|
||||
| 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?)`,只接受 direct v2 Thread / Turn / Item notification 与 message delta。 |
|
||||
| Event source | `nextEvent(timeoutMs?)` 或 `drainEvents(limit?)`,接受 direct v2 lifecycle notification、message delta 与 typed `error` signal。 |
|
||||
| Mock / fixture | 只允许测试显式传入,不允许 production transport fallback。 |
|
||||
|
||||
如果 gateway 方法来自 class instance,必须在宿主里包成闭包,避免丢失 `this`。本包不会替宿主绑定私有 client 实例。
|
||||
@@ -200,7 +201,7 @@ Product App business context
|
||||
最小 runtime client conformance:
|
||||
|
||||
- lifecycle:`startTurn -> steerTurn / readThread -> cancelTurn / respondAction` 均委托 gateway。
|
||||
- events:`subscribeLifecycleEvents`、`dispatchEvent`、`nextEvent` 只把 direct v2 Thread / Turn / Item 与 message delta 送入 lifecycle pipeline;raw channel 一律拒绝。
|
||||
- events:`subscribeLifecycleEvents` 只接收 direct v2 Thread / Turn / Item 与 message delta;`subscribeSignalEvents` 独立接收 typed `error`,不进入 lifecycle pipeline;raw channel 一律拒绝。
|
||||
- evidence:`exportEvidence` 有实现时委托,没有实现时 fail closed。
|
||||
- errors:transport error 原样传播,不切 mock。
|
||||
- bundle:`@limecloud/agent-runtime-client/sessionGateway` dist 不包含 Node builtin 或 sidecar/stdio 模块;只允许依赖 browser-safe App Server entry。
|
||||
|
||||
@@ -5,6 +5,8 @@ export {
|
||||
type AgentRuntimeLifecycleEventListener,
|
||||
type AgentRuntimeLifecycleNotification,
|
||||
type AgentRuntimeNotification,
|
||||
type AgentRuntimeSignalEventListener,
|
||||
type AgentRuntimeSignalNotification,
|
||||
type AgentSessionActionRespondParams,
|
||||
type AgentSessionActionRespondResponse,
|
||||
type ThreadReadParams,
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
type AgentRuntimeLifecycleEventListener,
|
||||
type AgentRuntimeLifecycleNotification,
|
||||
type AgentRuntimeNotification,
|
||||
type AgentRuntimeSignalEventListener,
|
||||
type AgentRuntimeSignalNotification,
|
||||
type JsonRpcError,
|
||||
type AgentSessionActionRespondParams,
|
||||
type AgentSessionActionRespondResponse,
|
||||
@@ -31,7 +33,8 @@ import {
|
||||
type JsonRpcMessage,
|
||||
type RequestId,
|
||||
type StructuredOutputContract,
|
||||
serverNotification,
|
||||
agentRuntimeLifecycleNotification,
|
||||
agentRuntimeSignalNotification,
|
||||
} from "@limecloud/app-server-client";
|
||||
|
||||
export type { StructuredOutputContract };
|
||||
@@ -57,6 +60,7 @@ export class AppServerAgentRuntimeClient implements AgentRuntimeClient {
|
||||
readonly connection: AppServerConnection;
|
||||
readonly #base: BaseAppServerAgentRuntimeClient;
|
||||
readonly #lifecycleListeners = new Set<AgentRuntimeLifecycleEventListener>();
|
||||
readonly #signalListeners = new Set<AgentRuntimeSignalEventListener>();
|
||||
readonly #eventPipeline: AgentRuntimeEventPipeline;
|
||||
readonly #pendingNextEvents: AgentRuntimeNotification[] = [];
|
||||
|
||||
@@ -158,8 +162,24 @@ export class AppServerAgentRuntimeClient implements AgentRuntimeClient {
|
||||
};
|
||||
}
|
||||
|
||||
subscribeSignalEvents(
|
||||
listener: AgentRuntimeSignalEventListener,
|
||||
): AgentRuntimeClientSubscription {
|
||||
this.#signalListeners.add(listener);
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
this.#signalListeners.delete(listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async dispatchEvent(message: JsonRpcMessage): Promise<boolean> {
|
||||
const lifecycle = serverNotification(message);
|
||||
const signal = agentRuntimeSignalNotification(message);
|
||||
if (signal) {
|
||||
await this.#dispatchSignal(signal);
|
||||
return true;
|
||||
}
|
||||
const lifecycle = agentRuntimeLifecycleNotification(message);
|
||||
if (lifecycle) {
|
||||
const result = await this.#dispatchLifecycle(lifecycle);
|
||||
return result.accepted;
|
||||
@@ -167,6 +187,14 @@ export class AppServerAgentRuntimeClient implements AgentRuntimeClient {
|
||||
return false;
|
||||
}
|
||||
|
||||
async #dispatchSignal(
|
||||
notification: AgentRuntimeSignalNotification,
|
||||
): Promise<void> {
|
||||
for (const listener of this.#signalListeners) {
|
||||
await listener(notification, notification);
|
||||
}
|
||||
}
|
||||
|
||||
async #dispatchLifecycle(
|
||||
notification: AgentRuntimeLifecycleNotification,
|
||||
): Promise<AgentRuntimeEventPipelineResult> {
|
||||
@@ -189,7 +217,12 @@ export class AppServerAgentRuntimeClient implements AgentRuntimeClient {
|
||||
return pending;
|
||||
}
|
||||
const notification = await this.connection.nextNotification(timeoutMs);
|
||||
const lifecycle = serverNotification(notification);
|
||||
const signal = agentRuntimeSignalNotification(notification);
|
||||
if (signal) {
|
||||
await this.#dispatchSignal(signal);
|
||||
return signal;
|
||||
}
|
||||
const lifecycle = agentRuntimeLifecycleNotification(notification);
|
||||
if (lifecycle) {
|
||||
const result = await this.#dispatchLifecycle(lifecycle);
|
||||
if (result.accepted) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
serverNotification,
|
||||
agentRuntimeLifecycleNotification,
|
||||
agentRuntimeSignalNotification,
|
||||
AgentRuntimeClient,
|
||||
AgentRuntimeClientSubscription,
|
||||
AgentRuntimeLifecycleEventListener,
|
||||
AgentRuntimeNotification,
|
||||
AgentRuntimeSignalEventListener,
|
||||
AgentRuntimeSignalNotification,
|
||||
AgentSessionActionRespondParams,
|
||||
AgentSessionActionRespondResponse,
|
||||
ThreadReadParams,
|
||||
@@ -169,6 +172,9 @@ export function createAgentRuntimeClientFromSessionGateway(
|
||||
subscribeLifecycleEvents(listener) {
|
||||
return eventRouter.subscribeLifecycle(listener);
|
||||
},
|
||||
subscribeSignalEvents(listener) {
|
||||
return eventRouter.subscribeSignal(listener);
|
||||
},
|
||||
async dispatchEvent(message) {
|
||||
const result = await eventRouter.dispatch(message);
|
||||
return result.accepted;
|
||||
@@ -264,6 +270,7 @@ async function nextDrainedAgentRuntimeEvent(
|
||||
|
||||
class AgentRuntimeGatewayEventRouter {
|
||||
readonly #lifecycleListeners = new Set<AgentRuntimeLifecycleEventListener>();
|
||||
readonly #signalListeners = new Set<AgentRuntimeSignalEventListener>();
|
||||
readonly #eventPipeline: AgentRuntimeEventPipeline;
|
||||
readonly #pendingNextEvents: AgentRuntimeNotification[] = [];
|
||||
|
||||
@@ -287,10 +294,28 @@ class AgentRuntimeGatewayEventRouter {
|
||||
};
|
||||
}
|
||||
|
||||
subscribeSignal(
|
||||
listener: AgentRuntimeSignalEventListener,
|
||||
): AgentRuntimeClientSubscription {
|
||||
this.#signalListeners.add(listener);
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
this.#signalListeners.delete(listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async dispatch(
|
||||
message: JsonRpcMessage,
|
||||
): Promise<AgentRuntimeGatewayDispatchResult> {
|
||||
const lifecycle = serverNotification(message);
|
||||
const signal = agentRuntimeSignalNotification(message);
|
||||
if (signal) {
|
||||
for (const listener of this.#signalListeners) {
|
||||
await listener(signal, signal);
|
||||
}
|
||||
return acceptedSignalResult(signal);
|
||||
}
|
||||
const lifecycle = agentRuntimeLifecycleNotification(message);
|
||||
if (!lifecycle) {
|
||||
return { accepted: false, reason: "dropped" };
|
||||
}
|
||||
@@ -321,4 +346,22 @@ class AgentRuntimeGatewayEventRouter {
|
||||
}
|
||||
}
|
||||
|
||||
type AgentRuntimeGatewayDispatchResult = AgentRuntimeEventPipelineResult;
|
||||
type AgentRuntimeSignalDispatchResult = {
|
||||
accepted: true;
|
||||
notification: AgentRuntimeSignalNotification;
|
||||
notifications: [AgentRuntimeSignalNotification];
|
||||
};
|
||||
|
||||
type AgentRuntimeGatewayDispatchResult =
|
||||
| AgentRuntimeEventPipelineResult
|
||||
| AgentRuntimeSignalDispatchResult;
|
||||
|
||||
function acceptedSignalResult(
|
||||
signal: AgentRuntimeSignalNotification,
|
||||
): AgentRuntimeSignalDispatchResult {
|
||||
return {
|
||||
accepted: true,
|
||||
notification: signal,
|
||||
notifications: [signal],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -263,13 +263,20 @@ test("malformed direct lifecycle notifications fail closed with explicit mapper
|
||||
test("root client dispatches direct lifecycle and delta only to lifecycle listeners", async () => {
|
||||
const runtime = createRuntimeClient();
|
||||
const lifecycle = [];
|
||||
const signals = [];
|
||||
runtime.subscribeLifecycleEvents((event) => lifecycle.push(event));
|
||||
runtime.subscribeSignalEvents((event) => signals.push(event));
|
||||
|
||||
for (const notification of directToolLifecycle()) {
|
||||
assert.equal(await runtime.dispatchEvent(notification), true);
|
||||
}
|
||||
const delta = agentMessageDeltaNotification();
|
||||
assert.equal(await runtime.dispatchEvent(delta), true);
|
||||
assert.equal(await runtime.dispatchEvent(errorNotification(true)), true);
|
||||
assert.equal(
|
||||
await runtime.dispatchEvent(turnPlanUpdatedNotification()),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
await runtime.dispatchEvent(rawNotification("turn.completed")),
|
||||
false,
|
||||
@@ -285,6 +292,10 @@ test("root client dispatches direct lifecycle and delta only to lifecycle listen
|
||||
"item/agentMessage/delta",
|
||||
],
|
||||
);
|
||||
assert.deepEqual(signals, [
|
||||
errorNotification(true),
|
||||
turnPlanUpdatedNotification(),
|
||||
]);
|
||||
});
|
||||
|
||||
test("session gateway dispatches direct lifecycle and rejects wrapper lifecycle", async () => {
|
||||
@@ -292,7 +303,9 @@ test("session gateway dispatches direct lifecycle and rejects wrapper lifecycle"
|
||||
createMinimalSessionGateway(),
|
||||
);
|
||||
const lifecycle = [];
|
||||
const signals = [];
|
||||
runtime.subscribeLifecycleEvents((event) => lifecycle.push(event));
|
||||
runtime.subscribeSignalEvents((event) => signals.push(event));
|
||||
|
||||
assert.equal(
|
||||
await runtime.dispatchEvent(turnNotification("inProgress")),
|
||||
@@ -307,6 +320,11 @@ test("session gateway dispatches direct lifecycle and rejects wrapper lifecycle"
|
||||
await runtime.dispatchEvent(mcpToolCallProgressNotification()),
|
||||
true,
|
||||
);
|
||||
assert.equal(await runtime.dispatchEvent(errorNotification(false)), true);
|
||||
assert.equal(
|
||||
await runtime.dispatchEvent(turnPlanUpdatedNotification()),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
await runtime.dispatchEvent(rawNotification("turn.started")),
|
||||
false,
|
||||
@@ -321,6 +339,10 @@ test("session gateway dispatches direct lifecycle and rejects wrapper lifecycle"
|
||||
"item/mcpToolCall/progress",
|
||||
],
|
||||
);
|
||||
assert.deepEqual(signals, [
|
||||
errorNotification(false),
|
||||
turnPlanUpdatedNotification(),
|
||||
]);
|
||||
});
|
||||
|
||||
test("disabling sequence verification does not restore raw lifecycle", async () => {
|
||||
@@ -369,6 +391,28 @@ test("nextEvent consumes direct notifications from gateway sources", async () =>
|
||||
|
||||
const drainedEvent = await drainedRuntime.nextEvent();
|
||||
assert.equal(drainedEvent.method, "turn/started");
|
||||
|
||||
const directSignal = errorNotification(true);
|
||||
const signalRuntime = createAgentRuntimeClientFromSessionGateway({
|
||||
...createMinimalSessionGateway(),
|
||||
async nextEvent() {
|
||||
return directSignal;
|
||||
},
|
||||
});
|
||||
const signals = [];
|
||||
signalRuntime.subscribeSignalEvents((event) => signals.push(event));
|
||||
|
||||
const signalEvent = await signalRuntime.nextEvent();
|
||||
assert.equal(signalEvent.method, "error");
|
||||
assert.deepEqual(signals, [directSignal]);
|
||||
|
||||
const drainedSignalRuntime = createAgentRuntimeClientFromSessionGateway({
|
||||
...createMinimalSessionGateway(),
|
||||
async drainEvents() {
|
||||
return [{ method: "log/list", params: {} }, errorNotification(false)];
|
||||
},
|
||||
});
|
||||
assert.equal((await drainedSignalRuntime.nextEvent()).method, "error");
|
||||
});
|
||||
|
||||
test("sequence verifier fails closed for orphan direct tool completion", async () => {
|
||||
@@ -788,6 +832,35 @@ function agentMessageDeltaNotification(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function errorNotification(willRetry, overrides = {}) {
|
||||
return {
|
||||
method: "error",
|
||||
params: {
|
||||
error: { message: "provider stream reconnecting" },
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
willRetry,
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function turnPlanUpdatedNotification(overrides = {}) {
|
||||
return {
|
||||
method: "turn/plan/updated",
|
||||
params: {
|
||||
explanation: "继续执行",
|
||||
plan: [
|
||||
{ step: "读现状", status: "completed" },
|
||||
{ step: "补主链", status: "inProgress" },
|
||||
],
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function commandOutputDeltaNotification(overrides = {}) {
|
||||
return {
|
||||
method: "item/commandExecution/outputDelta",
|
||||
|
||||
@@ -50,13 +50,25 @@ export type AgentRuntimeLifecycleNotification = Extract<
|
||||
}
|
||||
>;
|
||||
|
||||
export type AgentRuntimeNotification = AgentRuntimeLifecycleNotification;
|
||||
export type AgentRuntimeSignalNotification = Extract<
|
||||
ServerNotification,
|
||||
{ method: "error" | "turn/plan/updated" }
|
||||
>;
|
||||
|
||||
export type AgentRuntimeNotification =
|
||||
| AgentRuntimeLifecycleNotification
|
||||
| AgentRuntimeSignalNotification;
|
||||
|
||||
export type AgentRuntimeLifecycleEventListener = (
|
||||
event: AgentRuntimeLifecycleNotification,
|
||||
notification: AgentRuntimeLifecycleNotification,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export type AgentRuntimeSignalEventListener = (
|
||||
event: AgentRuntimeSignalNotification,
|
||||
notification: AgentRuntimeSignalNotification,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export type AgentRuntimeClientOptions = {
|
||||
request?: AppServerRequestOptions;
|
||||
};
|
||||
@@ -109,12 +121,16 @@ export interface AgentRuntimeClient {
|
||||
subscribeLifecycleEvents(
|
||||
listener: AgentRuntimeLifecycleEventListener,
|
||||
): AgentRuntimeClientSubscription;
|
||||
subscribeSignalEvents(
|
||||
listener: AgentRuntimeSignalEventListener,
|
||||
): AgentRuntimeClientSubscription;
|
||||
dispatchEvent(message: JsonRpcMessage): Promise<boolean>;
|
||||
nextEvent(timeoutMs?: number): Promise<AgentRuntimeNotification>;
|
||||
}
|
||||
|
||||
export class AppServerAgentEventRouter {
|
||||
#lifecycleListeners = new Set<AgentRuntimeLifecycleEventListener>();
|
||||
#signalListeners = new Set<AgentRuntimeSignalEventListener>();
|
||||
|
||||
subscribeLifecycle(listener: AgentRuntimeLifecycleEventListener): () => void {
|
||||
this.#lifecycleListeners.add(listener);
|
||||
@@ -123,7 +139,21 @@ export class AppServerAgentEventRouter {
|
||||
};
|
||||
}
|
||||
|
||||
subscribeSignal(listener: AgentRuntimeSignalEventListener): () => void {
|
||||
this.#signalListeners.add(listener);
|
||||
return () => {
|
||||
this.#signalListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
async dispatch(message: JsonRpcMessage): Promise<boolean> {
|
||||
const signal = agentRuntimeSignalNotification(message);
|
||||
if (signal) {
|
||||
for (const listener of this.#signalListeners) {
|
||||
await listener(signal, signal);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const lifecycle = agentRuntimeLifecycleNotification(message);
|
||||
if (lifecycle) {
|
||||
for (const listener of this.#lifecycleListeners) {
|
||||
@@ -254,6 +284,13 @@ export class AppServerAgentRuntimeClient implements AgentRuntimeClient {
|
||||
return { unsubscribe };
|
||||
}
|
||||
|
||||
subscribeSignalEvents(
|
||||
listener: AgentRuntimeSignalEventListener,
|
||||
): AgentRuntimeClientSubscription {
|
||||
const unsubscribe = this.eventRouter.subscribeSignal(listener);
|
||||
return { unsubscribe };
|
||||
}
|
||||
|
||||
async dispatchEvent(message: JsonRpcMessage): Promise<boolean> {
|
||||
return await this.eventRouter.dispatch(message);
|
||||
}
|
||||
@@ -261,6 +298,11 @@ export class AppServerAgentRuntimeClient implements AgentRuntimeClient {
|
||||
async nextEvent(timeoutMs?: number): Promise<AgentRuntimeNotification> {
|
||||
for (;;) {
|
||||
const notification = await this.connection.nextNotification(timeoutMs);
|
||||
const signal = agentRuntimeSignalNotification(notification);
|
||||
if (signal) {
|
||||
await this.dispatchEvent(signal);
|
||||
return signal;
|
||||
}
|
||||
const lifecycle = agentRuntimeLifecycleNotification(notification);
|
||||
if (lifecycle) {
|
||||
await this.dispatchEvent(lifecycle);
|
||||
@@ -273,7 +315,28 @@ export class AppServerAgentRuntimeClient implements AgentRuntimeClient {
|
||||
export function agentRuntimeLifecycleNotification(
|
||||
message: JsonRpcMessage,
|
||||
): AgentRuntimeLifecycleNotification | undefined {
|
||||
return serverNotification(message);
|
||||
const notification = serverNotification(message);
|
||||
return notification && !isAgentRuntimeSignalNotification(notification)
|
||||
? notification
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function agentRuntimeSignalNotification(
|
||||
message: JsonRpcMessage,
|
||||
): AgentRuntimeSignalNotification | undefined {
|
||||
const notification = serverNotification(message);
|
||||
return notification && isAgentRuntimeSignalNotification(notification)
|
||||
? notification
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function isAgentRuntimeSignalNotification(
|
||||
notification: ServerNotification,
|
||||
): notification is AgentRuntimeSignalNotification {
|
||||
return (
|
||||
notification.method === "error" ||
|
||||
notification.method === "turn/plan/updated"
|
||||
);
|
||||
}
|
||||
|
||||
export function createAgentRuntimeClient(
|
||||
|
||||
@@ -77,6 +77,7 @@ export const METHOD_DIAGNOSTICS_TRACE_READ = "diagnostics/trace/read";
|
||||
export const METHOD_DIAGNOSTICS_WINDOWS_STARTUP_READ =
|
||||
"diagnostics/windowsStartup/read";
|
||||
export const METHOD_DISCORD_CHANNEL_PROBE = "discordChannel/probe";
|
||||
export const METHOD_ERROR = "error";
|
||||
export const METHOD_EVIDENCE_EXPORT = "evidence/export";
|
||||
export const METHOD_EXECUTION_PROCESS_DRAIN_OUTPUT =
|
||||
"executionProcess/drainOutput";
|
||||
@@ -176,6 +177,8 @@ export const METHOD_MCP_SERVER_ENABLED_SET = "mcpServer/enabled/set";
|
||||
export const METHOD_MCP_SERVER_IMPORT_FROM_APP = "mcpServer/importFromApp";
|
||||
export const METHOD_MCP_SERVER_LIST = "mcpServer/list";
|
||||
export const METHOD_MCP_SERVER_OAUTH_LOGIN = "mcpServer/oauth/login";
|
||||
export const METHOD_MCP_SERVER_OAUTH_LOGIN_COMPLETED =
|
||||
"mcpServer/oauthLogin/completed";
|
||||
export const METHOD_MCP_SERVER_START = "mcpServer/start";
|
||||
export const METHOD_MCP_SERVER_STOP = "mcpServer/stop";
|
||||
export const METHOD_MCP_SERVER_SYNC_ALL_TO_LIVE = "mcpServer/syncAllToLive";
|
||||
@@ -310,6 +313,7 @@ export const METHOD_SKILL_REMOTE_INSPECT = "skillRemote/inspect";
|
||||
export const METHOD_SKILL_REPOSITORY_DELETE = "skillRepository/delete";
|
||||
export const METHOD_SKILL_REPOSITORY_LIST = "skillRepository/list";
|
||||
export const METHOD_SKILL_REPOSITORY_SAVE = "skillRepository/save";
|
||||
export const METHOD_SKILLS_CHANGED = "skills/changed";
|
||||
export const METHOD_SOUL_STYLE_PACK_INSTALL = "soulStylePack/install";
|
||||
export const METHOD_SOUL_STYLE_PACK_LIST = "soulStylePack/list";
|
||||
export const METHOD_SOUL_STYLE_PACK_STATUS_SET = "soulStylePack/status/set";
|
||||
@@ -364,6 +368,7 @@ export const METHOD_THREAD_UNARCHIVED = "thread/unarchived";
|
||||
export const METHOD_THREAD_UNSUBSCRIBE = "thread/unsubscribe";
|
||||
export const METHOD_TURN_COMPLETED = "turn/completed";
|
||||
export const METHOD_TURN_INTERRUPT = "turn/interrupt";
|
||||
export const METHOD_TURN_PLAN_UPDATED = "turn/plan/updated";
|
||||
export const METHOD_TURN_START = "turn/start";
|
||||
export const METHOD_TURN_STARTED = "turn/started";
|
||||
export const METHOD_TURN_STEER = "turn/steer";
|
||||
@@ -639,6 +644,10 @@ export const GENERATED_APP_SERVER_METHODS = [
|
||||
kind: "request",
|
||||
method: "discordChannel/probe",
|
||||
},
|
||||
{
|
||||
kind: "notification",
|
||||
method: "error",
|
||||
},
|
||||
{
|
||||
kind: "request",
|
||||
method: "evidence/export",
|
||||
@@ -943,6 +952,10 @@ export const GENERATED_APP_SERVER_METHODS = [
|
||||
kind: "request",
|
||||
method: "mcpServer/oauth/login",
|
||||
},
|
||||
{
|
||||
kind: "notification",
|
||||
method: "mcpServer/oauthLogin/completed",
|
||||
},
|
||||
{
|
||||
kind: "request",
|
||||
method: "mcpServer/start",
|
||||
@@ -1415,6 +1428,10 @@ export const GENERATED_APP_SERVER_METHODS = [
|
||||
kind: "request",
|
||||
method: "skillRepository/save",
|
||||
},
|
||||
{
|
||||
kind: "notification",
|
||||
method: "skills/changed",
|
||||
},
|
||||
{
|
||||
kind: "request",
|
||||
method: "soulStylePack/install",
|
||||
@@ -1607,6 +1624,10 @@ export const GENERATED_APP_SERVER_METHODS = [
|
||||
kind: "request",
|
||||
method: "turn/interrupt",
|
||||
},
|
||||
{
|
||||
kind: "notification",
|
||||
method: "turn/plan/updated",
|
||||
},
|
||||
{
|
||||
kind: "request",
|
||||
method: "turn/start",
|
||||
@@ -4604,6 +4625,44 @@ export interface ClientResponse {
|
||||
result: unknown;
|
||||
}
|
||||
|
||||
export type CodexErrorInfo =
|
||||
| "badRequest"
|
||||
| "contextWindowExceeded"
|
||||
| "cyberPolicy"
|
||||
| "internalServerError"
|
||||
| "other"
|
||||
| "sandboxError"
|
||||
| "serverOverloaded"
|
||||
| "sessionBudgetExceeded"
|
||||
| "threadRollbackFailed"
|
||||
| "unauthorized"
|
||||
| "usageLimitExceeded"
|
||||
| {
|
||||
httpConnectionFailed: {
|
||||
httpStatusCode?: number | null;
|
||||
};
|
||||
}
|
||||
| {
|
||||
responseStreamConnectionFailed: {
|
||||
httpStatusCode?: number | null;
|
||||
};
|
||||
}
|
||||
| {
|
||||
responseStreamDisconnected: {
|
||||
httpStatusCode?: number | null;
|
||||
};
|
||||
}
|
||||
| {
|
||||
responseTooManyFailedAttempts: {
|
||||
httpStatusCode?: number | null;
|
||||
};
|
||||
}
|
||||
| {
|
||||
activeTurnNotSteerable: {
|
||||
turnKind: NonSteerableTurnKind;
|
||||
};
|
||||
};
|
||||
|
||||
export interface CollabAgentState {
|
||||
message?: null | string;
|
||||
status: CollabAgentStatus;
|
||||
@@ -5145,6 +5204,13 @@ export type EndpointKind =
|
||||
| "openai_compatible"
|
||||
| "provider_base_url";
|
||||
|
||||
export interface ErrorNotification {
|
||||
error: TurnError;
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
willRetry: boolean;
|
||||
}
|
||||
|
||||
export interface EvidenceExportParams {
|
||||
includeArtifacts?: boolean | null;
|
||||
includeEvents?: boolean | null;
|
||||
@@ -5948,6 +6014,13 @@ export interface McpServerListResponse {
|
||||
servers?: unknown[];
|
||||
}
|
||||
|
||||
export interface McpServerOauthLoginCompletedNotification {
|
||||
error?: null | string;
|
||||
name: string;
|
||||
success: boolean;
|
||||
threadId: null | string;
|
||||
}
|
||||
|
||||
export interface McpServerOauthLoginParams {
|
||||
name: string;
|
||||
scopes?: string[] | null;
|
||||
@@ -6897,6 +6970,8 @@ export interface ModelVerificationNotification {
|
||||
verifications: ModelVerification[];
|
||||
}
|
||||
|
||||
export type NonSteerableTurnKind = "compact" | "review";
|
||||
|
||||
export interface OpenDeepLinkPayload {
|
||||
action?: null | string;
|
||||
kind: string;
|
||||
@@ -7724,6 +7799,18 @@ export type ServerNotification =
|
||||
method: "warning";
|
||||
params: WarningNotification;
|
||||
}
|
||||
| {
|
||||
method: "error";
|
||||
params: ErrorNotification;
|
||||
}
|
||||
| {
|
||||
method: "skills/changed";
|
||||
params: SkillsChangedNotification;
|
||||
}
|
||||
| {
|
||||
method: "mcpServer/oauthLogin/completed";
|
||||
params: McpServerOauthLoginCompletedNotification;
|
||||
}
|
||||
| {
|
||||
method: "thread/started";
|
||||
params: ThreadStartedNotification;
|
||||
@@ -7760,6 +7847,10 @@ export type ServerNotification =
|
||||
method: "turn/completed";
|
||||
params: TurnCompletedNotification;
|
||||
}
|
||||
| {
|
||||
method: "turn/plan/updated";
|
||||
params: TurnPlanUpdatedNotification;
|
||||
}
|
||||
| {
|
||||
method: "item/started";
|
||||
params: ItemStartedNotification;
|
||||
@@ -8204,6 +8295,8 @@ export interface SkillWorkflowStep {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type SkillsChangedNotification = Record<string, never>;
|
||||
|
||||
export interface SleepItem {
|
||||
durationMs?: number | null;
|
||||
id: string;
|
||||
@@ -9096,7 +9189,7 @@ export interface TurnEnvironmentParams {
|
||||
|
||||
export interface TurnError {
|
||||
additionalDetails?: null | string;
|
||||
errorInfo?: unknown;
|
||||
codexErrorInfo?: CodexErrorInfo | null;
|
||||
message: string;
|
||||
}
|
||||
|
||||
@@ -9109,6 +9202,20 @@ export type TurnInterruptResponse = Record<string, unknown>;
|
||||
|
||||
export type TurnItemsView = "full" | "notLoaded" | "summary";
|
||||
|
||||
export interface TurnPlanStep {
|
||||
status: TurnPlanStepStatus;
|
||||
step: string;
|
||||
}
|
||||
|
||||
export type TurnPlanStepStatus = "completed" | "inProgress" | "pending";
|
||||
|
||||
export interface TurnPlanUpdatedNotification {
|
||||
explanation?: null | string;
|
||||
plan: TurnPlanStep[];
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
}
|
||||
|
||||
export interface TurnStartParams {
|
||||
additionalContext?: null | Record<string, unknown>;
|
||||
approvalPolicy?: unknown;
|
||||
|
||||
@@ -8,6 +8,8 @@ export type RuntimeServerNotification = Extract<
|
||||
ServerNotification,
|
||||
{
|
||||
method:
|
||||
| "error"
|
||||
| "turn/plan/updated"
|
||||
| "thread/started"
|
||||
| "turn/started"
|
||||
| "turn/completed"
|
||||
@@ -30,6 +32,16 @@ export type ModelListUpdatedServerNotification = Extract<
|
||||
{ method: "model/list/updated" }
|
||||
>;
|
||||
|
||||
export type SkillsChangedServerNotification = Extract<
|
||||
ServerNotification,
|
||||
{ method: "skills/changed" }
|
||||
>;
|
||||
|
||||
export type McpServerOauthLoginCompletedServerNotification = Extract<
|
||||
ServerNotification,
|
||||
{ method: "mcpServer/oauthLogin/completed" }
|
||||
>;
|
||||
|
||||
export type ServerNotificationFor<
|
||||
Method extends RuntimeServerNotification["method"],
|
||||
> = Extract<RuntimeServerNotification, { method: Method }>;
|
||||
@@ -42,6 +54,14 @@ export function serverNotification(
|
||||
}
|
||||
|
||||
switch (message.method) {
|
||||
case "error":
|
||||
return hasErrorNotification(message.params)
|
||||
? (message as ServerNotificationFor<"error">)
|
||||
: undefined;
|
||||
case "turn/plan/updated":
|
||||
return hasTurnPlanUpdatedNotification(message.params)
|
||||
? (message as ServerNotificationFor<"turn/plan/updated">)
|
||||
: undefined;
|
||||
case "thread/started":
|
||||
return hasEntityId(record(message.params)?.thread)
|
||||
? (message as ServerNotificationFor<"thread/started">)
|
||||
@@ -146,12 +166,65 @@ export function isModelListUpdatedNotification(
|
||||
return modelListUpdatedServerNotification(message) !== undefined;
|
||||
}
|
||||
|
||||
export function skillsChangedServerNotification(
|
||||
message: JsonRpcMessage,
|
||||
): SkillsChangedServerNotification | undefined {
|
||||
if (!isJsonRpcNotification(message) || message.method !== "skills/changed") {
|
||||
return undefined;
|
||||
}
|
||||
const params = record(message.params);
|
||||
if (!params || Object.keys(params).length !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
return message as SkillsChangedServerNotification;
|
||||
}
|
||||
|
||||
export function isSkillsChangedNotification(
|
||||
message: JsonRpcMessage,
|
||||
): message is SkillsChangedServerNotification {
|
||||
return skillsChangedServerNotification(message) !== undefined;
|
||||
}
|
||||
|
||||
export function mcpServerOauthLoginCompletedServerNotification(
|
||||
message: JsonRpcMessage,
|
||||
): McpServerOauthLoginCompletedServerNotification | undefined {
|
||||
if (
|
||||
!isJsonRpcNotification(message) ||
|
||||
message.method !== "mcpServer/oauthLogin/completed"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const params = record(message.params);
|
||||
if (
|
||||
!hasOnlyKeys(params, ["error", "name", "success", "threadId"]) ||
|
||||
!hasString(params, "name") ||
|
||||
typeof params?.success !== "boolean" ||
|
||||
!hasRequiredNullableString(params, "threadId") ||
|
||||
!hasOptionalString(params, "error")
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return message as McpServerOauthLoginCompletedServerNotification;
|
||||
}
|
||||
|
||||
export function isThreadStartedNotification(
|
||||
message: JsonRpcMessage,
|
||||
): message is ServerNotificationFor<"thread/started"> {
|
||||
return serverNotification(message)?.method === "thread/started";
|
||||
}
|
||||
|
||||
export function isErrorNotification(
|
||||
message: JsonRpcMessage,
|
||||
): message is ServerNotificationFor<"error"> {
|
||||
return serverNotification(message)?.method === "error";
|
||||
}
|
||||
|
||||
export function isTurnPlanUpdatedNotification(
|
||||
message: JsonRpcMessage,
|
||||
): message is ServerNotificationFor<"turn/plan/updated"> {
|
||||
return serverNotification(message)?.method === "turn/plan/updated";
|
||||
}
|
||||
|
||||
export function isTurnStartedNotification(
|
||||
message: JsonRpcMessage,
|
||||
): message is ServerNotificationFor<"turn/started"> {
|
||||
@@ -246,6 +319,133 @@ function hasTurnNotification(value: unknown, statuses: string[]): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function hasErrorNotification(value: unknown): boolean {
|
||||
const params = record(value);
|
||||
const error = record(params?.error);
|
||||
const additionalDetails = error?.additionalDetails;
|
||||
const codexErrorInfo = error?.codexErrorInfo;
|
||||
return (
|
||||
hasOnlyKeys(params, ["error", "threadId", "turnId", "willRetry"]) &&
|
||||
hasOnlyKeys(error, ["additionalDetails", "codexErrorInfo", "message"]) &&
|
||||
hasString(params, "threadId") &&
|
||||
hasString(params, "turnId") &&
|
||||
hasString(error, "message") &&
|
||||
typeof params?.willRetry === "boolean" &&
|
||||
hasCodexErrorInfo(codexErrorInfo) &&
|
||||
(additionalDetails === undefined ||
|
||||
additionalDetails === null ||
|
||||
typeof additionalDetails === "string")
|
||||
);
|
||||
}
|
||||
|
||||
function hasTurnPlanUpdatedNotification(value: unknown): boolean {
|
||||
const params = record(value);
|
||||
if (
|
||||
!hasOnlyKeys(params, ["explanation", "plan", "threadId", "turnId"]) ||
|
||||
!hasString(params, "threadId") ||
|
||||
!hasString(params, "turnId") ||
|
||||
!Array.isArray(params?.plan) ||
|
||||
(params.explanation !== undefined &&
|
||||
params.explanation !== null &&
|
||||
typeof params.explanation !== "string")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return params.plan.every((value) => {
|
||||
const step = record(value);
|
||||
return (
|
||||
hasOnlyKeys(step, ["status", "step"]) &&
|
||||
hasString(step, "step") &&
|
||||
(step?.status === "pending" ||
|
||||
step?.status === "inProgress" ||
|
||||
step?.status === "completed")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function hasOnlyKeys(
|
||||
value: Record<string, unknown> | undefined,
|
||||
allowedKeys: string[],
|
||||
): boolean {
|
||||
return Boolean(
|
||||
value && Object.keys(value).every((key) => allowedKeys.includes(key)),
|
||||
);
|
||||
}
|
||||
|
||||
function hasRequiredNullableString(
|
||||
value: Record<string, unknown> | undefined,
|
||||
key: string,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
value &&
|
||||
Object.prototype.hasOwnProperty.call(value, key) &&
|
||||
(value[key] === null || hasString(value, key)),
|
||||
);
|
||||
}
|
||||
|
||||
function hasOptionalString(
|
||||
value: Record<string, unknown> | undefined,
|
||||
key: string,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
value &&
|
||||
(!Object.prototype.hasOwnProperty.call(value, key) ||
|
||||
typeof value[key] === "string"),
|
||||
);
|
||||
}
|
||||
|
||||
function hasCodexErrorInfo(value: unknown): boolean {
|
||||
if (value === undefined || value === null) {
|
||||
return true;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return [
|
||||
"badRequest",
|
||||
"contextWindowExceeded",
|
||||
"cyberPolicy",
|
||||
"internalServerError",
|
||||
"other",
|
||||
"sandboxError",
|
||||
"serverOverloaded",
|
||||
"sessionBudgetExceeded",
|
||||
"threadRollbackFailed",
|
||||
"unauthorized",
|
||||
"usageLimitExceeded",
|
||||
].includes(value);
|
||||
}
|
||||
|
||||
const variant = record(value);
|
||||
if (!variant || Object.keys(variant).length !== 1) {
|
||||
return false;
|
||||
}
|
||||
const variantName = Object.keys(variant)[0];
|
||||
const details = record(variant[variantName]);
|
||||
if (!details) {
|
||||
return false;
|
||||
}
|
||||
if (variantName === "activeTurnNotSteerable") {
|
||||
return details.turnKind === "review" || details.turnKind === "compact";
|
||||
}
|
||||
if (
|
||||
![
|
||||
"httpConnectionFailed",
|
||||
"responseStreamConnectionFailed",
|
||||
"responseStreamDisconnected",
|
||||
"responseTooManyFailedAttempts",
|
||||
].includes(variantName)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const status = details.httpStatusCode;
|
||||
return (
|
||||
status === undefined ||
|
||||
status === null ||
|
||||
(Number.isInteger(status) &&
|
||||
(status as number) >= 0 &&
|
||||
(status as number) <= 65_535)
|
||||
);
|
||||
}
|
||||
|
||||
function hasItemNotification(
|
||||
value: unknown,
|
||||
timestampKey: "startedAtMs" | "completedAtMs",
|
||||
|
||||
@@ -3479,6 +3479,37 @@ test("agent runtime client facade subscribes to direct lifecycle notifications",
|
||||
assert.equal(received[0].message.method, "turn/started");
|
||||
});
|
||||
|
||||
test("agent runtime client facade returns typed errors through the signal channel", async () => {
|
||||
const notification = {
|
||||
method: "error",
|
||||
params: {
|
||||
error: { message: "provider stream reconnecting" },
|
||||
threadId: "thread_external",
|
||||
turnId: "turn_external",
|
||||
willRetry: true,
|
||||
},
|
||||
};
|
||||
const runtime = new AppServerAgentRuntimeClient(
|
||||
new AppServerConnection({
|
||||
send() {},
|
||||
async nextMessage() {
|
||||
return notification;
|
||||
},
|
||||
}),
|
||||
);
|
||||
const signals = [];
|
||||
const subscription = runtime.subscribeSignalEvents((event, message) => {
|
||||
signals.push({ event, message });
|
||||
});
|
||||
|
||||
assert.equal(await runtime.dispatchEvent(notification), true);
|
||||
assert.equal((await runtime.nextEvent()).method, "error");
|
||||
subscription.unsubscribe();
|
||||
assert.equal(signals.length, 2);
|
||||
assert.equal(signals[0].event.params.willRetry, true);
|
||||
assert.equal(signals[0].message.method, "error");
|
||||
});
|
||||
|
||||
test("agent runtime client facade subscribes to direct item lifecycle", async () => {
|
||||
const notification = {
|
||||
method: "item/started",
|
||||
@@ -4628,6 +4659,53 @@ test("routes direct lifecycle notifications without wrapper projection", async (
|
||||
]);
|
||||
});
|
||||
|
||||
test("routes typed error notifications through the signal router", async () => {
|
||||
const notification = {
|
||||
method: "error",
|
||||
params: {
|
||||
error: { message: "provider stream reconnecting" },
|
||||
threadId: "thread_external",
|
||||
turnId: "turn_external",
|
||||
willRetry: true,
|
||||
},
|
||||
};
|
||||
const router = new AppServerAgentEventRouter();
|
||||
const signals = [];
|
||||
const unsubscribeSignal = router.subscribeSignal((event) => {
|
||||
signals.push(event);
|
||||
});
|
||||
|
||||
assert.equal(agentRuntimeLifecycleNotification(notification), undefined);
|
||||
assert.equal(await router.dispatch(notification), true);
|
||||
unsubscribeSignal();
|
||||
assert.deepEqual(signals, [notification]);
|
||||
});
|
||||
|
||||
test("routes turn plan updates only through the signal router", async () => {
|
||||
const notification = {
|
||||
method: "turn/plan/updated",
|
||||
params: {
|
||||
explanation: "继续执行",
|
||||
plan: [
|
||||
{ step: "读现状", status: "completed" },
|
||||
{ step: "补主链", status: "inProgress" },
|
||||
],
|
||||
threadId: "thread_external",
|
||||
turnId: "turn_external",
|
||||
},
|
||||
};
|
||||
const router = new AppServerAgentEventRouter();
|
||||
const lifecycle = [];
|
||||
const signals = [];
|
||||
router.subscribeLifecycle((event) => lifecycle.push(event));
|
||||
router.subscribeSignal((event) => signals.push(event));
|
||||
|
||||
assert.equal(agentRuntimeLifecycleNotification(notification), undefined);
|
||||
assert.equal(await router.dispatch(notification), true);
|
||||
assert.deepEqual(lifecycle, []);
|
||||
assert.deepEqual(signals, [notification]);
|
||||
});
|
||||
|
||||
test("connection buffers request responses read by idle notification loop", async () => {
|
||||
const sent = [];
|
||||
const waiters = [];
|
||||
|
||||
@@ -3,6 +3,7 @@ import { test } from "vitest";
|
||||
import {
|
||||
isAgentMessageDeltaNotification,
|
||||
isCommandExecutionOutputDeltaNotification,
|
||||
isErrorNotification,
|
||||
isFileChangePatchUpdatedNotification,
|
||||
isItemCompletedNotification,
|
||||
isItemStartedNotification,
|
||||
@@ -13,19 +14,121 @@ import {
|
||||
isReasoningSummaryTextDeltaNotification,
|
||||
isReasoningTextDeltaNotification,
|
||||
isServerNotification,
|
||||
isSkillsChangedNotification,
|
||||
isThreadStartedNotification,
|
||||
isThreadSettingsUpdatedNotification,
|
||||
isTurnPlanUpdatedNotification,
|
||||
isTurnCompletedNotification,
|
||||
isTurnStartedNotification,
|
||||
modelListUpdatedServerNotification,
|
||||
mcpServerOauthLoginCompletedServerNotification,
|
||||
serverNotification,
|
||||
skillsChangedServerNotification,
|
||||
} from "../dist/index.js";
|
||||
|
||||
const threadId = "thread-1";
|
||||
const turnId = "turn-1";
|
||||
|
||||
test("recognizes strict skills/changed catalog invalidation", () => {
|
||||
const notification = { method: "skills/changed", params: {} };
|
||||
assert.deepEqual(skillsChangedServerNotification(notification), notification);
|
||||
assert.equal(isSkillsChangedNotification(notification), true);
|
||||
|
||||
for (const malformed of [
|
||||
{ method: "skills/changed" },
|
||||
{ method: "skills/changed", params: null },
|
||||
{ method: "skills/changed", params: { path: "/private/skill" } },
|
||||
]) {
|
||||
assert.equal(skillsChangedServerNotification(malformed), undefined);
|
||||
assert.equal(isSkillsChangedNotification(malformed), false);
|
||||
}
|
||||
});
|
||||
|
||||
test("recognizes strict mcpServer/oauthLogin/completed notifications", () => {
|
||||
const success = {
|
||||
method: "mcpServer/oauthLogin/completed",
|
||||
params: {
|
||||
name: "remote-docs",
|
||||
threadId: null,
|
||||
success: true,
|
||||
},
|
||||
};
|
||||
const failure = {
|
||||
method: "mcpServer/oauthLogin/completed",
|
||||
params: {
|
||||
name: "remote-docs",
|
||||
threadId: "thread-1",
|
||||
success: false,
|
||||
error: "scope rejected",
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
mcpServerOauthLoginCompletedServerNotification(success),
|
||||
success,
|
||||
);
|
||||
assert.deepEqual(
|
||||
mcpServerOauthLoginCompletedServerNotification(failure),
|
||||
failure,
|
||||
);
|
||||
|
||||
for (const malformed of [
|
||||
{ method: "mcpServer/oauthLogin/completed" },
|
||||
{
|
||||
method: "mcpServer/oauthLogin/completed",
|
||||
params: { name: "remote-docs", success: true },
|
||||
},
|
||||
{
|
||||
method: "mcpServer/oauthLogin/completed",
|
||||
params: { name: "", threadId: null, success: true },
|
||||
},
|
||||
{
|
||||
method: "mcpServer/oauthLogin/completed",
|
||||
params: { name: "remote-docs", threadId: 7, success: true },
|
||||
},
|
||||
{
|
||||
method: "mcpServer/oauthLogin/completed",
|
||||
params: {
|
||||
name: "remote-docs",
|
||||
threadId: null,
|
||||
success: false,
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "mcpServer/oauthLogin/completed",
|
||||
params: {
|
||||
name: "remote-docs",
|
||||
threadId: null,
|
||||
success: true,
|
||||
serverName: "legacy",
|
||||
},
|
||||
},
|
||||
]) {
|
||||
assert.equal(
|
||||
mcpServerOauthLoginCompletedServerNotification(malformed),
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("recognizes native v2 lifecycle and reasoning notifications", () => {
|
||||
const notifications = [
|
||||
{
|
||||
method: "error",
|
||||
params: {
|
||||
error: {
|
||||
message: "provider stream reconnecting",
|
||||
codexErrorInfo: {
|
||||
responseStreamDisconnected: { httpStatusCode: null },
|
||||
},
|
||||
additionalDetails: null,
|
||||
},
|
||||
threadId,
|
||||
turnId,
|
||||
willRetry: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "thread/started",
|
||||
params: { thread: { id: threadId } },
|
||||
@@ -44,6 +147,18 @@ test("recognizes native v2 lifecycle and reasoning notifications", () => {
|
||||
turn: { id: turnId, status: "completed" },
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "turn/plan/updated",
|
||||
params: {
|
||||
explanation: "继续执行",
|
||||
plan: [
|
||||
{ step: "读取现状", status: "completed" },
|
||||
{ step: "补齐主链", status: "inProgress" },
|
||||
],
|
||||
threadId,
|
||||
turnId,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "item/started",
|
||||
params: {
|
||||
@@ -130,28 +245,30 @@ test("recognizes native v2 lifecycle and reasoning notifications", () => {
|
||||
];
|
||||
|
||||
assert.equal(notifications.every(isServerNotification), true);
|
||||
assert.equal(isThreadStartedNotification(notifications[0]), true);
|
||||
assert.equal(isTurnStartedNotification(notifications[1]), true);
|
||||
assert.equal(isTurnCompletedNotification(notifications[2]), true);
|
||||
assert.equal(isItemStartedNotification(notifications[3]), true);
|
||||
assert.equal(isItemCompletedNotification(notifications[4]), true);
|
||||
assert.equal(isAgentMessageDeltaNotification(notifications[5]), true);
|
||||
assert.equal(isErrorNotification(notifications[0]), true);
|
||||
assert.equal(isThreadStartedNotification(notifications[1]), true);
|
||||
assert.equal(isTurnStartedNotification(notifications[2]), true);
|
||||
assert.equal(isTurnCompletedNotification(notifications[3]), true);
|
||||
assert.equal(isTurnPlanUpdatedNotification(notifications[4]), true);
|
||||
assert.equal(isItemStartedNotification(notifications[5]), true);
|
||||
assert.equal(isItemCompletedNotification(notifications[6]), true);
|
||||
assert.equal(isAgentMessageDeltaNotification(notifications[7]), true);
|
||||
assert.equal(
|
||||
isCommandExecutionOutputDeltaNotification(notifications[6]),
|
||||
isCommandExecutionOutputDeltaNotification(notifications[8]),
|
||||
true,
|
||||
);
|
||||
assert.equal(isFileChangePatchUpdatedNotification(notifications[7]), true);
|
||||
assert.equal(isPlanDeltaNotification(notifications[8]), true);
|
||||
assert.equal(isMcpToolCallProgressNotification(notifications[9]), true);
|
||||
assert.equal(isFileChangePatchUpdatedNotification(notifications[9]), true);
|
||||
assert.equal(isPlanDeltaNotification(notifications[10]), true);
|
||||
assert.equal(isMcpToolCallProgressNotification(notifications[11]), true);
|
||||
assert.equal(
|
||||
isReasoningSummaryTextDeltaNotification(notifications[10]),
|
||||
isReasoningSummaryTextDeltaNotification(notifications[12]),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isReasoningSummaryPartAddedNotification(notifications[11]),
|
||||
isReasoningSummaryPartAddedNotification(notifications[13]),
|
||||
true,
|
||||
);
|
||||
assert.equal(isReasoningTextDeltaNotification(notifications[12]), true);
|
||||
assert.equal(isReasoningTextDeltaNotification(notifications[14]), true);
|
||||
assert.equal(
|
||||
isThreadSettingsUpdatedNotification({
|
||||
method: "thread/settings/updated",
|
||||
@@ -168,6 +285,87 @@ test("recognizes native v2 lifecycle and reasoning notifications", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("fails closed for malformed turn plan updates", () => {
|
||||
const malformed = [
|
||||
{
|
||||
method: "turn/plan/updated",
|
||||
params: {
|
||||
plan: [{ step: "补齐主链", status: "running" }],
|
||||
threadId,
|
||||
turnId,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "turn/plan/updated",
|
||||
params: {
|
||||
plan: [{ step: "补齐主链", status: "inProgress", extra: true }],
|
||||
threadId,
|
||||
turnId,
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "turn/plan/updated",
|
||||
params: {
|
||||
plan: [{ step: "", status: "completed" }],
|
||||
threadId,
|
||||
turnId,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
assert.equal(
|
||||
malformed.every((message) => !isTurnPlanUpdatedNotification(message)),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("fails closed for malformed typed error notifications", () => {
|
||||
const malformed = [
|
||||
{ error: { message: "failed" }, threadId, turnId },
|
||||
{ error: { message: "failed" }, threadId, turnId, willRetry: "false" },
|
||||
{ error: { message: "" }, threadId, turnId, willRetry: false },
|
||||
{ error: { message: "failed" }, threadId: "", turnId, willRetry: false },
|
||||
{ error: { message: "failed" }, threadId, turnId: "", willRetry: false },
|
||||
{
|
||||
error: { message: "failed", additionalDetails: 42 },
|
||||
threadId,
|
||||
turnId,
|
||||
willRetry: false,
|
||||
},
|
||||
{
|
||||
error: {
|
||||
message: "failed",
|
||||
codexErrorInfo: "responseStreamDisconnected",
|
||||
},
|
||||
threadId,
|
||||
turnId,
|
||||
willRetry: false,
|
||||
},
|
||||
{
|
||||
error: { message: "failed", legacyCode: "retryable" },
|
||||
threadId,
|
||||
turnId,
|
||||
willRetry: false,
|
||||
},
|
||||
{
|
||||
error: { message: "failed" },
|
||||
retryable: true,
|
||||
threadId,
|
||||
turnId,
|
||||
willRetry: false,
|
||||
},
|
||||
].map((params) => ({ method: "error", params }));
|
||||
|
||||
assert.equal(
|
||||
malformed.every((message) => !isServerNotification(message)),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
malformed.every((message) => !isErrorNotification(message)),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("recognizes typed model list update notifications", () => {
|
||||
const providerUpdate = {
|
||||
method: "model/list/updated",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@limecloud/lime-cli",
|
||||
"version": "1.117.0",
|
||||
"version": "1.118.0",
|
||||
"description": "Lime 官方任务 CLI",
|
||||
"bin": {
|
||||
"lime": "scripts/run.js"
|
||||
|
||||
@@ -41,6 +41,10 @@ import {
|
||||
TERMINAL_STALE_GUARD_FIRST_PROMPT,
|
||||
TERMINAL_STALE_GUARD_SCENARIO,
|
||||
TERMINAL_STALE_GUARD_SECOND_PROMPT,
|
||||
TYPED_ERROR_RETRY_FAILURE_PROMPT,
|
||||
TYPED_ERROR_RETRY_FAILURE_SCENARIO,
|
||||
TYPED_ERROR_RETRY_SUCCESS_PROMPT,
|
||||
TYPED_ERROR_RETRY_SUCCESS_SCENARIO,
|
||||
WEB_TOOLS_RENDERING_PROMPT,
|
||||
} from "./claw-chat-current-fixture-constants.mjs";
|
||||
import { EXPERT_PANEL_SKILLS_RUNTIME_UI_SKILL_REF } from "./claw-chat-current-fixture-expert-actions.mjs";
|
||||
@@ -113,6 +117,12 @@ export function resolveGateBExpectedIdentity({
|
||||
}
|
||||
|
||||
function resolveScenarioTurnStart(summary, options) {
|
||||
if (
|
||||
options?.scenario === TYPED_ERROR_RETRY_SUCCESS_SCENARIO ||
|
||||
options?.scenario === TYPED_ERROR_RETRY_FAILURE_SCENARIO
|
||||
) {
|
||||
return summary?.typedErrorTurnStart ?? null;
|
||||
}
|
||||
if (options?.scenario === "skills-runtime") {
|
||||
return summary?.manualEnableSkillsRuntimeTurnStart?.backend ?? null;
|
||||
}
|
||||
@@ -370,6 +380,12 @@ export function buildAssertionContext({
|
||||
entry.kind === "turnStart" &&
|
||||
entry.inputText === TERMINAL_STALE_GUARD_SECOND_PROMPT,
|
||||
);
|
||||
const typedErrorTurnStart = backendLedger.find(
|
||||
(entry) =>
|
||||
entry.kind === "turnStart" &&
|
||||
(entry.inputText === TYPED_ERROR_RETRY_SUCCESS_PROMPT ||
|
||||
entry.inputText === TYPED_ERROR_RETRY_FAILURE_PROMPT),
|
||||
);
|
||||
const mcpStructuredContentTurnStart = backendLedger.find(
|
||||
(entry) =>
|
||||
entry.kind === "turnStart" &&
|
||||
@@ -451,6 +467,12 @@ export function buildAssertionContext({
|
||||
options.scenario === TERMINAL_FAILED_AFTER_ANSWER_SCENARIO;
|
||||
const isTerminalStaleGuardScenario =
|
||||
options.scenario === TERMINAL_STALE_GUARD_SCENARIO;
|
||||
const isTypedErrorRetrySuccessScenario =
|
||||
options.scenario === TYPED_ERROR_RETRY_SUCCESS_SCENARIO;
|
||||
const isTypedErrorRetryFailureScenario =
|
||||
options.scenario === TYPED_ERROR_RETRY_FAILURE_SCENARIO;
|
||||
const isTypedErrorRetryScenario =
|
||||
isTypedErrorRetrySuccessScenario || isTypedErrorRetryFailureScenario;
|
||||
const isMcpStructuredContentScenario =
|
||||
options.scenario === "mcp-structured-content";
|
||||
const isMediaReferenceScenario =
|
||||
@@ -502,19 +524,21 @@ export function buildAssertionContext({
|
||||
? terminalCanceledAfterAnswerTurnStart?.runtimeRequest
|
||||
: isTerminalFailedAfterAnswerScenario
|
||||
? terminalFailedAfterAnswerTurnStart?.runtimeRequest
|
||||
: isMcpStructuredContentScenario
|
||||
? mcpStructuredContentTurnStart?.runtimeRequest
|
||||
: isMediaReferenceScenario
|
||||
? mediaReferenceTurnStart?.runtimeRequest
|
||||
: isSkillsRuntimeScenario
|
||||
? skillsRuntimeTurnStart?.runtimeRequest
|
||||
: isAnyExpertSkillsRuntimeScenario
|
||||
? expertRuntimeTurnStartForAssertions?.runtimeRequest
|
||||
: isContentFactoryArticleWorkspaceScenario
|
||||
? {}
|
||||
: isHomeHotpathScenario
|
||||
? homeHotpathTurnStart?.runtimeRequest
|
||||
: newsTurnStart?.runtimeRequest) ??
|
||||
: isTypedErrorRetryScenario
|
||||
? typedErrorTurnStart?.runtimeRequest
|
||||
: isMcpStructuredContentScenario
|
||||
? mcpStructuredContentTurnStart?.runtimeRequest
|
||||
: isMediaReferenceScenario
|
||||
? mediaReferenceTurnStart?.runtimeRequest
|
||||
: isSkillsRuntimeScenario
|
||||
? skillsRuntimeTurnStart?.runtimeRequest
|
||||
: isAnyExpertSkillsRuntimeScenario
|
||||
? expertRuntimeTurnStartForAssertions?.runtimeRequest
|
||||
: isContentFactoryArticleWorkspaceScenario
|
||||
? {}
|
||||
: isHomeHotpathScenario
|
||||
? homeHotpathTurnStart?.runtimeRequest
|
||||
: newsTurnStart?.runtimeRequest) ??
|
||||
{};
|
||||
const hasCancelPhase = isCancelOnlyScenario || isCancelThenContinueScenario;
|
||||
const goalHarness = readHarnessMetadataFromTurnStart(goalTurnStart);
|
||||
@@ -588,38 +612,43 @@ export function buildAssertionContext({
|
||||
: isTerminalFailedAfterAnswerScenario
|
||||
? terminalFailedAfterAnswerTurnStart?.inputText ===
|
||||
TERMINAL_FAILED_AFTER_ANSWER_PROMPT
|
||||
: isTerminalStaleGuardScenario
|
||||
? terminalStaleGuardFirstTurnStart?.inputText ===
|
||||
TERMINAL_STALE_GUARD_FIRST_PROMPT &&
|
||||
terminalStaleGuardSecondTurnStart?.inputText ===
|
||||
TERMINAL_STALE_GUARD_SECOND_PROMPT
|
||||
: isMcpStructuredContentScenario
|
||||
? mcpStructuredContentTurnStart?.inputText ===
|
||||
MCP_STRUCTURED_CONTENT_PROMPT
|
||||
: isMediaReferenceScenario
|
||||
? mediaReferenceTurnStart?.inputText ===
|
||||
MEDIA_REFERENCE_PROMPT
|
||||
: isSkillsRuntimeScenario
|
||||
? skillsRuntimeTurnStart?.inputText ===
|
||||
SKILLS_RUNTIME_PROMPT &&
|
||||
explicitSkillsRuntimeTurnStart?.inputText ===
|
||||
SKILLS_RUNTIME_EXPLICIT_PROMPT &&
|
||||
manualEnableSkillsRuntimeTurnStart?.inputText ===
|
||||
SKILLS_RUNTIME_MANUAL_ENABLE_PROMPT
|
||||
: isAnyExpertSkillsRuntimeScenario
|
||||
? isExpertPanelSkillsRuntimeScenario
|
||||
? expertPanelSkillsRuntimeTurnStart?.inputText ===
|
||||
EXPERT_SKILLS_RUNTIME_PANEL_PROMPT
|
||||
: expertSkillsRuntimeTurnStart?.inputText?.includes(
|
||||
EXPERT_SKILLS_RUNTIME_PROMPT,
|
||||
) === true
|
||||
: isContentFactoryArticleWorkspaceScenario
|
||||
? true
|
||||
: isSoulStyleScenario
|
||||
? newsTraceTurnStart?.inputText ===
|
||||
NEWS_PROMPT
|
||||
: newsTurnStart?.inputText ===
|
||||
NEWS_PROMPT;
|
||||
: isTypedErrorRetryScenario
|
||||
? typedErrorTurnStart?.inputText ===
|
||||
(isTypedErrorRetrySuccessScenario
|
||||
? TYPED_ERROR_RETRY_SUCCESS_PROMPT
|
||||
: TYPED_ERROR_RETRY_FAILURE_PROMPT)
|
||||
: isTerminalStaleGuardScenario
|
||||
? terminalStaleGuardFirstTurnStart?.inputText ===
|
||||
TERMINAL_STALE_GUARD_FIRST_PROMPT &&
|
||||
terminalStaleGuardSecondTurnStart?.inputText ===
|
||||
TERMINAL_STALE_GUARD_SECOND_PROMPT
|
||||
: isMcpStructuredContentScenario
|
||||
? mcpStructuredContentTurnStart?.inputText ===
|
||||
MCP_STRUCTURED_CONTENT_PROMPT
|
||||
: isMediaReferenceScenario
|
||||
? mediaReferenceTurnStart?.inputText ===
|
||||
MEDIA_REFERENCE_PROMPT
|
||||
: isSkillsRuntimeScenario
|
||||
? skillsRuntimeTurnStart?.inputText ===
|
||||
SKILLS_RUNTIME_PROMPT &&
|
||||
explicitSkillsRuntimeTurnStart?.inputText ===
|
||||
SKILLS_RUNTIME_EXPLICIT_PROMPT &&
|
||||
manualEnableSkillsRuntimeTurnStart?.inputText ===
|
||||
SKILLS_RUNTIME_MANUAL_ENABLE_PROMPT
|
||||
: isAnyExpertSkillsRuntimeScenario
|
||||
? isExpertPanelSkillsRuntimeScenario
|
||||
? expertPanelSkillsRuntimeTurnStart?.inputText ===
|
||||
EXPERT_SKILLS_RUNTIME_PANEL_PROMPT
|
||||
: expertSkillsRuntimeTurnStart?.inputText?.includes(
|
||||
EXPERT_SKILLS_RUNTIME_PROMPT,
|
||||
) === true
|
||||
: isContentFactoryArticleWorkspaceScenario
|
||||
? true
|
||||
: isSoulStyleScenario
|
||||
? newsTraceTurnStart?.inputText ===
|
||||
NEWS_PROMPT
|
||||
: newsTurnStart?.inputText ===
|
||||
NEWS_PROMPT;
|
||||
return {
|
||||
backendLedger,
|
||||
traceMessages,
|
||||
@@ -656,6 +685,7 @@ export function buildAssertionContext({
|
||||
terminalFailedAfterAnswerTurnStart,
|
||||
terminalStaleGuardFirstTurnStart,
|
||||
terminalStaleGuardSecondTurnStart,
|
||||
typedErrorTurnStart,
|
||||
mcpStructuredContentTurnStart,
|
||||
mediaReferenceTurnStart,
|
||||
skillsRuntimeTurnStart,
|
||||
@@ -686,6 +716,9 @@ export function buildAssertionContext({
|
||||
isTerminalCanceledAfterAnswerScenario,
|
||||
isTerminalFailedAfterAnswerScenario,
|
||||
isTerminalStaleGuardScenario,
|
||||
isTypedErrorRetrySuccessScenario,
|
||||
isTypedErrorRetryFailureScenario,
|
||||
isTypedErrorRetryScenario,
|
||||
isMcpStructuredContentScenario,
|
||||
isMediaReferenceScenario,
|
||||
isSkillsRuntimeScenario,
|
||||
|
||||
@@ -5,13 +5,25 @@ import { buildNotApplicableAssertions } from "./claw-chat-current-fixture-not-ap
|
||||
import { buildScenarioAssertions } from "./claw-chat-current-fixture-scenario-assertions.mjs";
|
||||
import { assert } from "./claw-chat-current-fixture-utils.mjs";
|
||||
import { buildGateBContractAssertions } from "./claw-chat-current-fixture-gate-b-contract.mjs";
|
||||
import {
|
||||
buildTurnPlanUpdateScenarioAssertions,
|
||||
TURN_PLAN_UPDATE_SCENARIO,
|
||||
} from "./claw-chat-current-fixture-turn-plan-update.mjs";
|
||||
|
||||
export function buildFixtureAssertionReport(input) {
|
||||
const backendSummary = summarizeBackendLedger(input.backendLedger);
|
||||
const context = buildAssertionContext(input);
|
||||
const commonAssertions = buildCommonAssertions(context);
|
||||
const scenarioAssertions = buildScenarioAssertions(context);
|
||||
const notApplicableAssertions = buildNotApplicableAssertions(context);
|
||||
const isTurnPlanUpdateScenario =
|
||||
input.options?.scenario === TURN_PLAN_UPDATE_SCENARIO;
|
||||
const commonAssertions = isTurnPlanUpdateScenario
|
||||
? {}
|
||||
: buildCommonAssertions(context);
|
||||
const scenarioAssertions = isTurnPlanUpdateScenario
|
||||
? buildTurnPlanUpdateScenarioAssertions(context)
|
||||
: buildScenarioAssertions(context);
|
||||
const notApplicableAssertions = isTurnPlanUpdateScenario
|
||||
? []
|
||||
: buildNotApplicableAssertions(context);
|
||||
const gateBContractAssertions = buildGateBContractAssertions(
|
||||
context.gateBContract,
|
||||
);
|
||||
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
NEWS_PROMPT,
|
||||
TEXT_PROVIDER_FIXTURE_API_KEY,
|
||||
TEXT_FIXTURE_PROVIDER_NAME,
|
||||
TURN_PLAN_UPDATE_DONE_TEXT,
|
||||
TURN_PLAN_UPDATE_EXPLANATION,
|
||||
TURN_PLAN_UPDATE_PROMPT,
|
||||
TURN_PLAN_UPDATE_STEPS,
|
||||
TURN_PLAN_UPDATE_TOOL_CALL_ID,
|
||||
} from "./claw-chat-current-fixture-constants.mjs";
|
||||
import { writeFixtureBackend } from "./claw-chat-current-fixture-backend-script.mjs";
|
||||
import {
|
||||
@@ -87,6 +92,10 @@ export function createTempRuntimeEnv() {
|
||||
const backendPath = path.join(tempRoot, "claw-chat-backend.mjs");
|
||||
const backendLedgerPath = path.join(tempRoot, "claw-chat-backend.jsonl");
|
||||
const cancelSignalPath = path.join(tempRoot, "claw-chat-cancel.signal");
|
||||
const typedErrorSignalPath = path.join(
|
||||
tempRoot,
|
||||
"claw-chat-typed-error.signal",
|
||||
);
|
||||
const mediaReferenceSourcePath = path.join(
|
||||
tempRoot,
|
||||
"fixture-media-reference.png",
|
||||
@@ -116,6 +125,7 @@ export function createTempRuntimeEnv() {
|
||||
writeFixtureConfig(homeConfigPath);
|
||||
writeFixtureConfig(macConfigPath);
|
||||
fs.writeFileSync(backendLedgerPath, "");
|
||||
fs.writeFileSync(typedErrorSignalPath, "");
|
||||
fs.writeFileSync(
|
||||
mediaReferenceSourcePath,
|
||||
Buffer.from(IMAGE_PROVIDER_FIXTURE_DATA_URL.split(",")[1] || "", "base64"),
|
||||
@@ -129,6 +139,7 @@ export function createTempRuntimeEnv() {
|
||||
backendPath,
|
||||
backendLedgerPath,
|
||||
cancelSignalPath,
|
||||
typedErrorSignalPath,
|
||||
mediaReferenceSourcePath,
|
||||
configPath,
|
||||
macConfigPath,
|
||||
@@ -266,6 +277,12 @@ export async function startImageProviderFixtureServer() {
|
||||
|
||||
function fixtureTextForChatRequest(body) {
|
||||
const serialized = typeof body === "string" ? body : JSON.stringify(body);
|
||||
if (
|
||||
serialized.includes(TURN_PLAN_UPDATE_PROMPT) &&
|
||||
serialized.includes("Plan updated")
|
||||
) {
|
||||
return `执行清单已更新,主链继续运行。\n${TURN_PLAN_UPDATE_DONE_TEXT}`;
|
||||
}
|
||||
if (serialized.includes(ACTIVE_STEER_INPUT)) {
|
||||
return `${ACTIVE_STEER_FINAL_TEXT}\n${ACTIVE_STEER_DONE_TEXT}`;
|
||||
}
|
||||
@@ -347,6 +364,7 @@ function summarizeChatCompletionRequestBody(body, expectedSoulStyle) {
|
||||
parsed = {};
|
||||
}
|
||||
const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
||||
const tools = Array.isArray(parsed.tools) ? parsed.tools : [];
|
||||
const serialized = body || "";
|
||||
return {
|
||||
stream: parsed.stream ?? null,
|
||||
@@ -355,6 +373,22 @@ function summarizeChatCompletionRequestBody(body, expectedSoulStyle) {
|
||||
responseFormatType:
|
||||
parsed.response_format?.type ?? parsed.responseFormat?.type ?? null,
|
||||
toolChoice: parsed.tool_choice ?? parsed.toolChoice ?? null,
|
||||
toolNames: tools
|
||||
.map((tool) => tool?.function?.name ?? tool?.name ?? null)
|
||||
.filter(Boolean),
|
||||
bodyIncludesUpdatePlanToolDefinition: tools.some(
|
||||
(tool) => (tool?.function?.name ?? tool?.name) === "update_plan",
|
||||
),
|
||||
bodyIncludesTurnPlanUpdatePrompt: messages.some(
|
||||
(message) =>
|
||||
message?.role === "user" &&
|
||||
readChatContentText(message?.content).includes(TURN_PLAN_UPDATE_PROMPT),
|
||||
),
|
||||
bodyIncludesPlanUpdatedToolResult: messages.some(
|
||||
(message) =>
|
||||
message?.role === "tool" &&
|
||||
readChatContentText(message?.content).includes("Plan updated"),
|
||||
),
|
||||
soulMarkers: summarizeSoulPromptMarkers(serialized, expectedSoulStyle),
|
||||
bodyIncludesPresentationContract:
|
||||
serialized.includes("image_task_presentation.v1") ||
|
||||
@@ -444,7 +478,45 @@ function openaiChatCompletionSseBody(content) {
|
||||
return `data: ${firstChunk}\n\ndata: ${finalChunk}\n\ndata: ${usageChunk}\n\ndata: [DONE]\n\n`;
|
||||
}
|
||||
|
||||
function openaiModelsBody() {
|
||||
function openaiToolCallSseBody({ name, callId, argumentsValue }) {
|
||||
const toolChunk = JSON.stringify({
|
||||
id: "chatcmpl-claw-text-fixture",
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: FIXTURE_MODEL,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
role: "assistant",
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: callId,
|
||||
type: "function",
|
||||
function: {
|
||||
name,
|
||||
arguments: JSON.stringify(argumentsValue),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const finalChunk = JSON.stringify({
|
||||
id: "chatcmpl-claw-text-fixture",
|
||||
object: "chat.completion.chunk",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: FIXTURE_MODEL,
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }],
|
||||
});
|
||||
const usageChunk = JSON.stringify(openaiChatCompletionUsageChunk());
|
||||
return `data: ${toolChunk}\n\ndata: ${finalChunk}\n\ndata: ${usageChunk}\n\ndata: [DONE]\n\n`;
|
||||
}
|
||||
|
||||
function openaiModelsBody({ supportsTools = false } = {}) {
|
||||
return {
|
||||
object: "list",
|
||||
data: [
|
||||
@@ -456,10 +528,13 @@ function openaiModelsBody() {
|
||||
input_modalities: ["text", "image"],
|
||||
output_modalities: ["text"],
|
||||
task_families: ["chat", "vision_understanding"],
|
||||
runtime_features: ["streaming"],
|
||||
runtime_features: supportsTools
|
||||
? ["streaming", "tool_calling"]
|
||||
: ["streaming"],
|
||||
capabilities: {
|
||||
vision: true,
|
||||
streaming: true,
|
||||
tools: supportsTools,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -469,6 +544,7 @@ function openaiModelsBody() {
|
||||
export async function startTextProviderFixtureServer({
|
||||
soulStyleExpectation = null,
|
||||
activeSteerDelayMs = 0,
|
||||
turnPlanUpdate = false,
|
||||
} = {}) {
|
||||
const requests = [];
|
||||
const expectedAuthorization = `Bearer ${TEXT_PROVIDER_FIXTURE_API_KEY}`;
|
||||
@@ -512,7 +588,9 @@ export async function startTextProviderFixtureServer({
|
||||
["/models", "/v1/models"].includes(pathname)
|
||||
) {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify(openaiModelsBody()));
|
||||
response.end(
|
||||
JSON.stringify(openaiModelsBody({ supportsTools: turnPlanUpdate })),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -530,6 +608,44 @@ export async function startTextProviderFixtureServer({
|
||||
} catch {
|
||||
parsedBody = {};
|
||||
}
|
||||
const messages = Array.isArray(parsedBody.messages)
|
||||
? parsedBody.messages
|
||||
: [];
|
||||
const hasTurnPlanPrompt = messages.some(
|
||||
(message) =>
|
||||
message?.role === "user" &&
|
||||
readChatContentText(message?.content).includes(
|
||||
TURN_PLAN_UPDATE_PROMPT,
|
||||
),
|
||||
);
|
||||
const hasPlanUpdatedToolResult = messages.some(
|
||||
(message) =>
|
||||
message?.role === "tool" &&
|
||||
readChatContentText(message?.content).includes("Plan updated"),
|
||||
);
|
||||
if (
|
||||
turnPlanUpdate &&
|
||||
parsedBody.stream !== false &&
|
||||
hasTurnPlanPrompt &&
|
||||
!hasPlanUpdatedToolResult
|
||||
) {
|
||||
const responseBody = openaiToolCallSseBody({
|
||||
argumentsValue: {
|
||||
explanation: TURN_PLAN_UPDATE_EXPLANATION,
|
||||
plan: TURN_PLAN_UPDATE_STEPS,
|
||||
},
|
||||
callId: TURN_PLAN_UPDATE_TOOL_CALL_ID,
|
||||
name: "update_plan",
|
||||
});
|
||||
response.writeHead(200, {
|
||||
"content-type": "text/event-stream; charset=utf-8",
|
||||
"cache-control": "no-cache",
|
||||
connection: "close",
|
||||
"content-length": Buffer.byteLength(responseBody),
|
||||
});
|
||||
response.end(responseBody);
|
||||
return;
|
||||
}
|
||||
const content = fixtureTextForChatRequest(body);
|
||||
if (parsedBody.stream === false) {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user