feat(m1-08): stream safe execution summaries

This commit is contained in:
2026-08-03 23:10:49 +08:00
parent ffda402861
commit 9fd321c41c
8 changed files with 547 additions and 40 deletions
+35 -4
View File
@@ -116,6 +116,10 @@ export type TaskProgress = {
cancellable: boolean;
};
/** Display-only, adapter-authoritative execution trace. It grants no ability. */
export type AgentExecutionStep = { id: string; title: string; status: "running" | "completed" | "failed" };
export type AgentExecutionTrace = { execution_id: string; status: "running" | "completed" | "failed"; steps: readonly AgentExecutionStep[] };
/**
* A user-originated action awaiting delivery through the Transport Adapter.
* It is data only: the Store owns retries and the Kernel owns state changes.
@@ -180,8 +184,9 @@ type ConversationItemBase = {
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: "markdown"; markdown: string; execution_id?: string })
| (ConversationItemBase & { kind: "agent-status"; status: string; detail: string; execution_id?: string })
| (ConversationItemBase & { kind: "execution-summary"; trace: AgentExecutionTrace })
| (ConversationItemBase & { kind: "progress"; title: string; percent: number; status: string; task?: TaskProgress })
| (ConversationItemBase & { kind: "error"; message: string; code?: string })
| (ConversationItemBase & { kind: "tool-call"; call: ToolCallRequest })
@@ -199,6 +204,7 @@ const ACTOR_KINDS = new Set<ActorKind>(["human", "agent", "app", "system"]);
const SUPPORTED_TYPES = new Set([
"lineup.v1.text",
"lineup.v1.agent.status",
"lineup.v1.agent.execution",
"lineup.v1.agent.progress",
"lineup.v1.error",
"lineup.v1.tool.call",
@@ -323,6 +329,7 @@ const TOOL_FINAL_STATUSES = new Set<ToolCallFinalStatus>(["completed", "failed",
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"]);
const EXECUTION_STATUSES = new Set(["running", "completed", "failed"]);
function optionalTimestamp(value: unknown): string | undefined {
return typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : undefined;
@@ -436,6 +443,24 @@ function parseTaskProgress(payload: JsonObject, percent: number): TaskProgress |
return { operation_id: operationID, title, percent, status, cancellable: payload.cancellable === true };
}
function parseExecutionTrace(payload: JsonObject): AgentExecutionTrace | undefined {
const executionID = identifier(payload.execution_id);
const status = text(payload.status);
if (!executionID || !EXECUTION_STATUSES.has(status) || !Array.isArray(payload.steps) || payload.steps.length > 12) return undefined;
const steps: AgentExecutionStep[] = [];
const seen = new Set<string>();
for (const raw of payload.steps) {
const step = object(raw);
const id = step && identifier(step.id);
const title = step && text(step.title).trim();
const stepStatus = step && text(step.status);
if (!id || !title || title.length > 160 || !stepStatus || !EXECUTION_STATUSES.has(stepStatus) || seen.has(id)) return undefined;
seen.add(id);
steps.push({ id, title, status: stepStatus as AgentExecutionStep["status"] });
}
return { execution_id: executionID, status: status as AgentExecutionTrace["status"], steps };
}
function parseToolResult(payload: JsonObject): ToolCallResult | undefined {
const callID = identifier(payload.call_id);
const status = text(payload.status) as ToolCallFinalStatus;
@@ -484,14 +509,20 @@ export function decodeConversationItem(raw: string): ConversationItem {
switch (envelope.type) {
case "lineup.v1.text": {
const markdown = text(payload.markdown) || text(payload.text);
return markdown ? { kind: "markdown", markdown, ...base } : fallbackItem({ code: "invalid_payload", detail: "Text payload requires text or markdown.", envelope });
return markdown
? { kind: "markdown", markdown, ...(identifier(payload.execution_id) ? { execution_id: identifier(payload.execution_id)! } : {}), ...base }
: fallbackItem({ code: "invalid_payload", detail: "Text payload requires text or markdown.", envelope });
}
case "lineup.v1.agent.status": {
const status = text(payload.status);
return status
? { kind: "agent-status", status, detail: text(payload.detail), ...base }
? { kind: "agent-status", status, detail: text(payload.detail), ...(identifier(payload.execution_id) ? { execution_id: identifier(payload.execution_id)! } : {}), ...base }
: fallbackItem({ code: "invalid_payload", detail: "Agent status payload requires status.", envelope });
}
case "lineup.v1.agent.execution": {
const trace = parseExecutionTrace(payload);
return trace ? { kind: "execution-summary", trace, ...base } : fallbackItem({ code: "invalid_payload", detail: "Agent execution requires bounded public steps.", envelope });
}
case "lineup.v1.agent.progress": {
const percent = payload.percent;
if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 0 || percent > 100) {