feat: aggregate task progress updates

This commit is contained in:
2026-08-03 16:17:15 +08:00
parent 9312b09e76
commit 10d95c4a5e
11 changed files with 273 additions and 13 deletions
+24 -2
View File
@@ -106,6 +106,16 @@ export type ToolCallCancel = {
reason: string;
};
export type TaskStatus = "running" | "waiting" | "completed" | "failed" | "cancelled";
export type TaskProgress = {
operation_id: string;
title: string;
percent: number;
status: TaskStatus;
cancellable: boolean;
};
/**
* A user-originated action awaiting delivery through the Transport Adapter.
* It is data only: the Store owns retries and the Kernel owns state changes.
@@ -172,7 +182,7 @@ export type ConversationItem =
| (ConversationItemBase & { kind: "user-message"; text: string; delivery: DeliveryState })
| (ConversationItemBase & { kind: "markdown"; markdown: string })
| (ConversationItemBase & { kind: "agent-status"; status: string; detail: string })
| (ConversationItemBase & { kind: "progress"; title: string; percent: number; status: string })
| (ConversationItemBase & { kind: "progress"; title: string; percent: number; status: string; task?: TaskProgress })
| (ConversationItemBase & { kind: "error"; message: string; code?: string })
| (ConversationItemBase & { kind: "tool-call"; call: ToolCallRequest })
| (ConversationItemBase & { kind: "tool-result"; result: ToolCallResult })
@@ -312,6 +322,7 @@ const TOOL_KINDS = new Set<ToolCallKind>(["choice", "confirm", "input"]);
const TOOL_FINAL_STATUSES = new Set<ToolCallFinalStatus>(["completed", "failed", "cancelled", "expired"]);
const ACTION_GROUP_MODES = new Set<ActionGroupMode>(["single-choice", "multi-choice", "button"]);
const INPUT_FIELD_TYPES = new Set<InputFieldType>(["text", "textarea", "number"]);
const TASK_STATUSES = new Set<TaskStatus>(["running", "waiting", "completed", "failed", "cancelled"]);
function optionalTimestamp(value: unknown): string | undefined {
return typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : undefined;
@@ -416,6 +427,15 @@ function parseToolCall(payload: JsonObject): ToolCallRequest | undefined {
};
}
function parseTaskProgress(payload: JsonObject, percent: number): TaskProgress | undefined {
const operationID = identifier(payload.operation_id);
const status = text(payload.status) as TaskStatus;
if (!operationID || !TASK_STATUSES.has(status)) return undefined;
const title = text(payload.title).trim();
if (!title || title.length > 200) return undefined;
return { operation_id: operationID, title, percent, status, cancellable: payload.cancellable === true };
}
function parseToolResult(payload: JsonObject): ToolCallResult | undefined {
const callID = identifier(payload.call_id);
const status = text(payload.status) as ToolCallFinalStatus;
@@ -477,7 +497,9 @@ export function decodeConversationItem(raw: string): ConversationItem {
if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 0 || percent > 100) {
return fallbackItem({ code: "invalid_payload", detail: "Agent progress payload requires percent between 0 and 100.", envelope });
}
return { kind: "progress", title: text(payload.title), percent, status: text(payload.status), ...base };
const task = payload.operation_id === undefined ? undefined : parseTaskProgress(payload, percent);
if (payload.operation_id !== undefined && !task) return fallbackItem({ code: "invalid_payload", detail: "Task progress requires operation_id, title, supported status, and valid cancellation flag.", envelope });
return { kind: "progress", title: text(payload.title), percent, status: text(payload.status), ...(task ? { task } : {}), ...base };
}
case "lineup.v1.error": {
const message = text(payload.message);