fix(runtime): persist miniapp tool progress

This commit is contained in:
2026-08-06 00:51:52 +08:00
parent 6fa350267a
commit 0b7257f3d3
5 changed files with 47 additions and 6 deletions
@@ -416,6 +416,28 @@ describe("LineUpRuntime", () => {
expect(secondTransport.sent.filter(entry => entry.payload.includes("restart-tool-call"))).toHaveLength(1); expect(secondTransport.sent.filter(entry => entry.payload.includes("restart-tool-call"))).toHaveLength(1);
}); });
it("restores the latest MiniApp progress after Runtime restart", async () => {
const conversationID = "user_1:2:channel_1";
const call = JSON.stringify({
v: 1, id: "progress-restart-call", type: "lineup.v1.miniapp.tool.call", conversation_id: conversationID,
sender: { kind: "agent", id: "agent_1" },
payload: { call_id: "progress-restart-call", tool_id: "task-dashboard.open", app_scope: "task-dashboard", inventory_revision: "catalog-1", input: { title: "进度恢复" } },
});
const storage = new MemoryStorage();
const firstTransport = new FakeTransport([[{ message_seq: 71, from_uid: "agent_1", payload: call }], []]);
const first = new LineUpRuntime({ storage, createTransport: () => firstTransport });
await first.login(login);
await first.syncOnce();
const instance = first.lifecycle.instances.activeFor("task-dashboard", conversationID)!;
const sdk = first.openBundledMiniApp("task-dashboard", instance.instance_id);
await sdk.tools.reportProgress("progress-restart-call", { percent: 42, status: "working", detail: "正在处理" });
expect(first.miniAppToolCalls()).toEqual([expect.objectContaining({ call_id: "progress-restart-call", status: "running", progress: { percent: 42, status: "working", detail: "正在处理" } })]);
const second = new LineUpRuntime({ storage, createTransport: () => new FakeTransport() });
await second.login(login);
expect(second.miniAppToolCalls()).toEqual([expect.objectContaining({ call_id: "progress-restart-call", status: "waiting_for_app", progress: { percent: 42, status: "working", detail: "正在处理" } })]);
});
it("projects Agent state and Chat-safe Runtime status through the SDK", async () => { it("projects Agent state and Chat-safe Runtime status through the SDK", async () => {
const conversationID = "user_1:2:channel_1"; const conversationID = "user_1:2:channel_1";
const transport = new FakeTransport([[ const transport = new FakeTransport([[
@@ -763,7 +763,7 @@ export class LineUpRuntime {
public reportMiniAppToolProgress(instanceID: string, callID: string, progress: MiniAppToolProgress): Promise<CommandReceipt> { public reportMiniAppToolProgress(instanceID: string, callID: string, progress: MiniAppToolProgress): Promise<CommandReceipt> {
if (!validProgress(progress)) return Promise.resolve({ disposition: "rejected", code: "invalid_action" }); if (!validProgress(progress)) return Promise.resolve({ disposition: "rejected", code: "invalid_action" });
const transition = this.miniappTools.progress(callID, instanceID, this.now()); const transition = this.miniappTools.progress(callID, instanceID, progress, this.now());
if (transition.disposition !== "accepted") return Promise.resolve({ disposition: "rejected", code: "invalid_action" }); if (transition.disposition !== "accepted") return Promise.resolve({ disposition: "rejected", code: "invalid_action" });
this.store.replaceMiniAppToolCalls(this.miniappTools.list()); this.store.replaceMiniAppToolCalls(this.miniappTools.list());
return this.queueProtocolAction("app-result", "lineup.v1.miniapp.tool.progress", { return this.queueProtocolAction("app-result", "lineup.v1.miniapp.tool.progress", {
@@ -19,4 +19,14 @@ describe("MiniAppToolStateMachine", () => {
expect(machine.cancel("call-2", "task:2", "app_closed", "2026-08-04T00:00:02.000Z")).toEqual(expect.objectContaining({ disposition: "invalid_scope" })); expect(machine.cancel("call-2", "task:2", "app_closed", "2026-08-04T00:00:02.000Z")).toEqual(expect.objectContaining({ disposition: "invalid_scope" }));
expect(machine.cancel("call-2", "task:1", "app_closed", "2026-08-04T00:00:02.000Z")).toMatchObject({ disposition: "accepted", record: { status: "cancelled", cancel_reason: "app_closed" } }); expect(machine.cancel("call-2", "task:1", "app_closed", "2026-08-04T00:00:02.000Z")).toMatchObject({ disposition: "accepted", record: { status: "cancelled", cancel_reason: "app_closed" } });
}); });
it("keeps the latest progress in the durable call record", () => {
const machine = new MiniAppToolStateMachine();
machine.receive({ call_id: "call-progress", tool_id: "task-dashboard.open", inventory_revision: "catalog-1", app_scope: "task-dashboard", instance_id: "task:1", conversation_id: "c", input: {}, created_at: "2026-08-04T00:00:00.000Z" });
machine.route("call-progress", "task:1", "2026-08-04T00:00:01.000Z");
expect(machine.progress("call-progress", "task:1", { percent: 42, status: "working", detail: "正在处理" }, "2026-08-04T00:00:02.000Z")).toMatchObject({ disposition: "accepted", record: { progress: { percent: 42, status: "working", detail: "正在处理" } } });
const restored = new MiniAppToolStateMachine();
restored.restore(machine.list(), "2026-08-04T00:00:03.000Z");
expect(restored.get("call-progress")).toMatchObject({ status: "waiting_for_app", progress: { percent: 42, status: "working", detail: "正在处理" } });
});
}); });
@@ -14,6 +14,12 @@ export type MiniAppToolStatus =
export type MiniAppToolCancelReason = "app_closed" | "agent_cancelled" | "runtime_cancelled"; export type MiniAppToolCancelReason = "app_closed" | "agent_cancelled" | "runtime_cancelled";
export type MiniAppToolProgress = {
percent?: number;
status?: string;
detail?: string;
};
export type MiniAppToolCallRecord = { export type MiniAppToolCallRecord = {
call_id: string; call_id: string;
tool_id: string; tool_id: string;
@@ -23,6 +29,7 @@ export type MiniAppToolCallRecord = {
conversation_id: string; conversation_id: string;
status: MiniAppToolStatus; status: MiniAppToolStatus;
input: JsonObject; input: JsonObject;
progress?: MiniAppToolProgress;
result?: JsonObject; result?: JsonObject;
error_code?: string; error_code?: string;
cancel_reason?: MiniAppToolCancelReason; cancel_reason?: MiniAppToolCancelReason;
@@ -66,10 +73,11 @@ export class MiniAppToolStateMachine {
}); });
} }
public progress(callID: string, instanceID: string, now: string): MiniAppToolTransition { public progress(callID: string, instanceID: string, progress: MiniAppToolProgress, now: string): MiniAppToolTransition {
return this.transition(callID, now, ["waiting_for_app", "running"], record => { return this.transition(callID, now, ["waiting_for_app", "running"], record => {
if (record.instance_id !== instanceID) throw new Error("instance mismatch"); if (record.instance_id !== instanceID) throw new Error("instance mismatch");
record.status = "running"; record.status = "running";
record.progress = clone(progress);
}); });
} }
@@ -24,7 +24,7 @@
| ✅ | P1 | A6 | 标准交互的超时、dismiss、提交和答案 schema 没有由 Runtime 统一裁决 | `lineup-runtime.ts``agent-interaction-service.ts``standard-interaction-contract.ts` | 已让本地提交、取消、过期和 Agent dismiss 同步推进 Interaction 与 Kernelnotice 也会完成 Kernel;四种答案在 Runtime 按固定 schema 校验,默认有效期和重启过期均只写一条 outbox。 | | ✅ | P1 | A6 | 标准交互的超时、dismiss、提交和答案 schema 没有由 Runtime 统一裁决 | `lineup-runtime.ts``agent-interaction-service.ts``standard-interaction-contract.ts` | 已让本地提交、取消、过期和 Agent dismiss 同步推进 Interaction 与 Kernelnotice 也会完成 Kernel;四种答案在 Runtime 按固定 schema 校验,默认有效期和重启过期均只写一条 outbox。 |
| ✅ | P2 | A7 | 迭代文档中的 Tool 名称和代码 Manifest 不一致 | 主文档 §5.2/§5.3;`reference-miniapps.ts` | 已统一为 `task-dashboard.open``task-dashboard.update``whiteboard.open``whiteboard.submit`Manifest schema、MiniApp 实现、sandbox iframe、fixture 和测试均已同步。 | | ✅ | P2 | A7 | 迭代文档中的 Tool 名称和代码 Manifest 不一致 | 主文档 §5.2/§5.3;`reference-miniapps.ts` | 已统一为 `task-dashboard.open``task-dashboard.update``whiteboard.open``whiteboard.submit`Manifest schema、MiniApp 实现、sandbox iframe、fixture 和测试均已同步。 |
| 🔴 | P2 | A8 | 当前 Task Dashboard/Whiteboard Surface 主要是 Host 硬编码的静态页面,无法证明真实业务 UI 在各自受限 Surface 内运行 | `main.ts:180-198``isolated-surface-host.ts` | 至少提供能代表业务边界的受限 Surface fixture,并验证 MiniApp 业务逻辑不能取得 Host DOM 或 Runtime 内部。 | | 🔴 | P2 | A8 | 当前 Task Dashboard/Whiteboard Surface 主要是 Host 硬编码的静态页面,无法证明真实业务 UI 在各自受限 Surface 内运行 | `main.ts:180-198``isolated-surface-host.ts` | 至少提供能代表业务边界的受限 Surface fixture,并验证 MiniApp 业务逻辑不能取得 Host DOM 或 Runtime 内部。 |
| 🔴 | P2 | A9 | MiniApp progress 没有进入可恢复的 Tool 状态记录 | `miniapp-tool-state.ts` | 如果契约要求进度可恢复,需持久化最后进度并覆盖 Runtime 重启;否则修改文档,明确 progress 只是一种可丢失事件。 | | | P2 | A9 | MiniApp progress 没有进入可恢复的 Tool 状态记录 | `miniapp-tool-state.ts``lineup-runtime.test.ts` | 已在 Tool 记录保存最后一次 `percent/status/detail`ConversationStore 随记录持久化;Runtime 重启后可恢复,且补充状态机和重启回归测试。 |
| 🔴 | P3 | A10 | App session 记录和 opened/closed 事件没有显式保存 `agent_id` / `conversation_id` 字段 | `app-instance-manager.ts``lineup-runtime.ts:203-209,1052-1058` | P3 也必须在本迭代处理:App session opened/closed 事件和持久化记录补齐两个上下文字段。 | | 🔴 | P3 | A10 | App session 记录和 opened/closed 事件没有显式保存 `agent_id` / `conversation_id` 字段 | `app-instance-manager.ts``lineup-runtime.ts:203-209,1052-1058` | P3 也必须在本迭代处理:App session opened/closed 事件和持久化记录补齐两个上下文字段。 |
| 🔴 | P3 | A11 | 旧的 `openExtensionApp` 公开入口仍提供较宽的旁路能力 | `lineup-runtime.ts:286-300` | P3 也必须在本迭代处理:删除公开旁路,或让它只返回受统一 SDK v1 约束的适配器。 | | 🔴 | P3 | A11 | 旧的 `openExtensionApp` 公开入口仍提供较宽的旁路能力 | `lineup-runtime.ts:286-300` | P3 也必须在本迭代处理:删除公开旁路,或让它只返回受统一 SDK v1 约束的适配器。 |
@@ -33,6 +33,7 @@
- `npm run build` 通过。 - `npm run build` 通过。
- `npm test -- --run` 通过:29 个测试文件、131 个测试(含 Tool 契约、默认有效期、重启过期和答案 schema 回归)。 - `npm test -- --run` 通过:29 个测试文件、131 个测试(含 Tool 契约、默认有效期、重启过期和答案 schema 回归)。
- `git diff --check` 通过。 - `git diff --check` 通过。
- MiniApp progress 回归通过:状态机保存最新进度,Runtime 重启后恢复 `percent/status/detail`
- 使用独立 `agent-browser` 会话登录本地测试账号成功。 - 使用独立 `agent-browser` 会话登录本地测试账号成功。
- 主 IM 页面、同步状态、应用子会话区域和“启用任务面板”入口可见。 - 主 IM 页面、同步状态、应用子会话区域和“启用任务面板”入口可见。
- 用户消息可以发送并显示“已发送”。 - 用户消息可以发送并显示“已发送”。
@@ -95,10 +96,10 @@ Interact 仍然可以使用可信 DOM,但它的 SDK 已明确拆成两层:`s
基础构建与自动化测试:通过 基础构建与自动化测试:通过
真实登录和消息发送:通过 真实登录和消息发送:通过
App 架构边界:部分通过(A1/A2/A5/A6 已实现,A2 待浏览器复验) App 架构边界:部分通过(A1/A2/A5/A6 已实现,A2 待浏览器复验)
MiniApp 隔离与 SDK 规范:A1A7 已通过,仍需处理后续 P2/P3 MiniApp 隔离与 SDK 规范:A1A9 已通过,仍需处理后续 P3
标准交互终态契约:通过 标准交互终态契约:通过
迭代整体验收:不通过,继续处理 A8A11;A2 需在最终浏览器复验中确认 迭代整体验收:不通过,继续处理 A8、A10、A11;A2 需在最终浏览器复验中确认
``` ```
本轮独立验收由子 agent 完成。A6 已在本轮修复并通过自动化验证,A7 已完成契约收敛并通过全量测试;下一轮继续处理 A8A11,并重新执行 本轮独立验收由子 agent 完成。A6 已在本轮修复并通过自动化验证,A7 已完成契约收敛并通过全量测试A9 已完成进度持久化并通过重启回归;下一轮继续处理 A8、A10、A11,并重新执行
独立验收。P2/P3 不能被静默删除,必须在本迭代结束前解决或保留明确的延期记录。 独立验收。P2/P3 不能被静默删除,必须在本迭代结束前解决或保留明确的延期记录。