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);
});
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 () => {
const conversationID = "user_1:2:channel_1";
const transport = new FakeTransport([[
@@ -763,7 +763,7 @@ export class LineUpRuntime {
public reportMiniAppToolProgress(instanceID: string, callID: string, progress: MiniAppToolProgress): Promise<CommandReceipt> {
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" });
this.store.replaceMiniAppToolCalls(this.miniappTools.list());
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: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 MiniAppToolProgress = {
percent?: number;
status?: string;
detail?: string;
};
export type MiniAppToolCallRecord = {
call_id: string;
tool_id: string;
@@ -23,6 +29,7 @@ export type MiniAppToolCallRecord = {
conversation_id: string;
status: MiniAppToolStatus;
input: JsonObject;
progress?: MiniAppToolProgress;
result?: JsonObject;
error_code?: string;
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 => {
if (record.instance_id !== instanceID) throw new Error("instance mismatch");
record.status = "running";
record.progress = clone(progress);
});
}