fix(runtime): persist miniapp tool progress
This commit is contained in:
@@ -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 与 Kernel;notice 也会完成 Kernel;四种答案在 Runtime 按固定 schema 校验,默认有效期和重启过期均只写一条 outbox。 |
|
| ✅ | P1 | A6 | 标准交互的超时、dismiss、提交和答案 schema 没有由 Runtime 统一裁决 | `lineup-runtime.ts`、`agent-interaction-service.ts`、`standard-interaction-contract.ts` | 已让本地提交、取消、过期和 Agent dismiss 同步推进 Interaction 与 Kernel;notice 也会完成 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 规范:A1~A7 已通过,仍需处理后续 P2/P3
|
MiniApp 隔离与 SDK 规范:A1~A9 已通过,仍需处理后续 P3
|
||||||
标准交互终态契约:通过
|
标准交互终态契约:通过
|
||||||
迭代整体验收:不通过,继续处理 A8~A11;A2 需在最终浏览器复验中确认
|
迭代整体验收:不通过,继续处理 A8、A10、A11;A2 需在最终浏览器复验中确认
|
||||||
```
|
```
|
||||||
|
|
||||||
本轮独立验收由子 agent 完成。A6 已在本轮修复并通过自动化验证,A7 已完成契约收敛并通过全量测试;下一轮继续处理 A8~A11,并重新执行
|
本轮独立验收由子 agent 完成。A6 已在本轮修复并通过自动化验证,A7 已完成契约收敛并通过全量测试,A9 已完成进度持久化并通过重启回归;下一轮继续处理 A8、A10、A11,并重新执行
|
||||||
独立验收。P2/P3 不能被静默删除,必须在本迭代结束前解决或保留明确的延期记录。
|
独立验收。P2/P3 不能被静默删除,必须在本迭代结束前解决或保留明确的延期记录。
|
||||||
|
|||||||
Reference in New Issue
Block a user