Merge pull request #2346 from pikasTech/fix/1445-lane-b-terminal-authority
fix(web): 收敛 Workbench terminal authority helper
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
import type { ChatMessage, ProjectionDiagnostic, ProviderProfile, WorkbenchSessionRecord } from "@/types";
|
||||
import { firstNonEmptyString } from "@/utils";
|
||||
import { messageHasSealedTerminalResult, messageIsSealedTerminal, turnStatusAuthorityIsSealed } from "./workbench-terminal-authority";
|
||||
|
||||
export interface ComposerState {
|
||||
disabled: boolean;
|
||||
@@ -82,10 +83,10 @@ export function resolveComposerState(input: { messages: ChatMessage[]; sessions?
|
||||
const turn = activeTraceId ? input.turnStatusAuthority?.[activeTraceId] : null;
|
||||
const activeByRequest = Boolean(input.chatPending && currentRequest && isActiveStatus(currentRequest.status ?? "running"));
|
||||
const activeByMessage = latestMessage?.role === "agent" && isActiveStatus(latestMessage.status);
|
||||
const terminalByMessage = messageIsTerminalForSession(latestMessage);
|
||||
const terminalByMessage = messageIsSealedTerminal(latestMessage);
|
||||
const turnStatus = normalizeSessionStatus(turn?.status);
|
||||
const terminalTurn = turn?.terminal === true || isTerminalStatus(turnStatus);
|
||||
const terminalByTurn = terminalTurn && messageHasTerminalResponse(latestMessage);
|
||||
const terminalByTurn = terminalTurn && messageHasSealedTerminalResult(latestMessage);
|
||||
const activeByStatus = !terminalByMessage && !terminalByTurn && (turn?.running === true || isActiveStatus(turnStatus) || activeByRequest || activeByMessage);
|
||||
const terminal = terminalByMessage || terminalByTurn;
|
||||
const active = activeSession(input.sessions ?? [], sessionId);
|
||||
@@ -106,7 +107,7 @@ export function resolveCancelableAgentMessage(input: { messages: ChatMessage[];
|
||||
if (message.role !== "agent") continue;
|
||||
if (firstNonEmptyString(message.traceId, message.runnerTrace?.traceId) !== targetTraceId) continue;
|
||||
if (!messageBelongsToCancelTarget(message, input)) continue;
|
||||
if (messageIsTerminalForSession(message)) continue;
|
||||
if (messageIsSealedTerminal(message)) continue;
|
||||
return message;
|
||||
}
|
||||
return null;
|
||||
@@ -312,27 +313,10 @@ function traceNeedsTurnStatusRefresh(traceId: string | null | undefined, message
|
||||
const turn = turnStatusAuthority?.[id];
|
||||
const message = latestMessageForTrace(messages, id);
|
||||
if (turnStatusAuthorityIsSealed(turn, message)) return false;
|
||||
if (messageIsTerminalForSession(message)) return false;
|
||||
if (messageIsSealedTerminal(message)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function turnStatusAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null): boolean {
|
||||
if (!(turn?.terminal === true || isTerminalStatus(turn?.status))) return false;
|
||||
return message ? messageHasTerminalResponse(message) : false;
|
||||
}
|
||||
|
||||
function messageIsTerminalForSession(message: ChatMessage | null | undefined): boolean {
|
||||
if (!message || message.role !== "agent") return false;
|
||||
const status = normalizeSessionStatus(message.status);
|
||||
return isTerminalStatus(status) && messageHasTerminalResponse(message);
|
||||
}
|
||||
|
||||
function messageHasTerminalResponse(message: ChatMessage | null | undefined): boolean {
|
||||
if (!message || message.role !== "agent") return false;
|
||||
const finalResponse = recordValue((message as Record<string, unknown>).finalResponse);
|
||||
return Boolean(firstNonEmptyString(message.text, message.content, finalResponse?.text, finalResponse?.content, finalResponse?.message));
|
||||
}
|
||||
|
||||
function resolveSessionTabStatus(session: WorkbenchSessionRecord, authority: SessionStatusAuthority | null | undefined): string {
|
||||
void session;
|
||||
const authorityStatus = normalizeSessionStatus(authority?.status);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import type { ChatMessage } from "@/types";
|
||||
import type { TurnStatusAuthority } from "./workbench-session";
|
||||
import { messageHasSealedTerminalResult, messageIsSealedTerminal, traceAuthorityIsSealed, turnStatusAuthorityIsSealed } from "./workbench-terminal-authority";
|
||||
|
||||
function agentMessage(input: Partial<ChatMessage> & Pick<ChatMessage, "status">): ChatMessage {
|
||||
return {
|
||||
id: `msg_${String(input.status)}`,
|
||||
role: "agent",
|
||||
title: "Agent",
|
||||
text: "",
|
||||
createdAt: "2026-07-02T00:00:00.000Z",
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
function turnAuthority(input: Partial<TurnStatusAuthority> = {}): TurnStatusAuthority {
|
||||
return {
|
||||
traceId: "trc_terminal",
|
||||
status: "completed",
|
||||
terminal: true,
|
||||
running: false,
|
||||
...input
|
||||
};
|
||||
}
|
||||
|
||||
test("terminal authority requires terminal status and a final body for sealed messages", () => {
|
||||
assert.equal(messageIsSealedTerminal(agentMessage({ status: "completed", text: "final answer" })), true);
|
||||
assert.equal(messageIsSealedTerminal(agentMessage({ status: "completed" })), false);
|
||||
assert.equal(messageIsSealedTerminal(agentMessage({ status: "running", text: "partial" })), false);
|
||||
});
|
||||
|
||||
test("turn authority seals when terminal turn has final response even if message status is stale running", () => {
|
||||
const turn = turnAuthority();
|
||||
const message = agentMessage({ status: "running", traceId: "trc_terminal", finalResponse: { text: "final answer" } });
|
||||
|
||||
assert.equal(turnStatusAuthorityIsSealed(turn, message), true);
|
||||
assert.equal(traceAuthorityIsSealed(turn, message), true);
|
||||
});
|
||||
|
||||
test("terminal result text can come from reply and finalText authority fields", () => {
|
||||
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", reply: { finalText: "done" } })), true);
|
||||
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", finalText: "done" })), true);
|
||||
});
|
||||
|
||||
test("terminal result keeps finalResponse text content and message field semantics", () => {
|
||||
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", finalResponse: { text: "from text" } })), true);
|
||||
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", finalResponse: { content: "from content" } })), true);
|
||||
assert.equal(messageHasSealedTerminalResult(agentMessage({ status: "running", finalResponse: { message: "from message" } })), true);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-unique-projection.
|
||||
// Responsibility: Shared terminal seal authority helpers for Workbench store reducers.
|
||||
|
||||
import type { ChatMessage } from "@/types";
|
||||
import { firstNonEmptyString } from "@/utils";
|
||||
import { finalResponseText, messageText, normalizedStatusText } from "./workbench-message-projection-runtime";
|
||||
import type { TurnStatusAuthority } from "./workbench-session";
|
||||
|
||||
export function traceAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null | undefined): boolean {
|
||||
return turnStatusAuthorityIsSealed(turn, message);
|
||||
}
|
||||
|
||||
export function turnStatusAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null | undefined): boolean {
|
||||
if (!(turn?.terminal === true || isTerminalAuthorityStatus(turn?.status))) return false;
|
||||
return messageHasSealedTerminalResult(message);
|
||||
}
|
||||
|
||||
export function messageIsSealedTerminal(message: ChatMessage | null | undefined): boolean {
|
||||
if (!message || message.role !== "agent") return false;
|
||||
return isTerminalAuthorityStatus(message.status) && messageHasSealedTerminalResult(message);
|
||||
}
|
||||
|
||||
export function messageHasSealedTerminalResult(message: ChatMessage | null | undefined): boolean {
|
||||
if (!message || message.role !== "agent") return false;
|
||||
const record = message as Record<string, unknown>;
|
||||
const reply = recordValue(record.reply);
|
||||
return Boolean(firstNonEmptyString(
|
||||
messageText(message.text),
|
||||
messageText(record.content),
|
||||
finalResponseText(record.finalResponse),
|
||||
finalResponseText(record.reply),
|
||||
messageText(reply?.finalText),
|
||||
messageText(record.finalText),
|
||||
messageText(record.final)
|
||||
));
|
||||
}
|
||||
|
||||
function recordValue(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function isTerminalAuthorityStatus(value: unknown): boolean {
|
||||
return [
|
||||
"completed",
|
||||
"failed",
|
||||
"blocked",
|
||||
"timeout",
|
||||
"canceled",
|
||||
"stale",
|
||||
"thread-resume-failed",
|
||||
"interrupted",
|
||||
"expired"
|
||||
].includes(normalizeTerminalAuthorityStatus(value) ?? "");
|
||||
}
|
||||
|
||||
function normalizeTerminalAuthorityStatus(value: unknown): string | null {
|
||||
const status = normalizedStatusText(value);
|
||||
if (status === "cancelled") return "canceled";
|
||||
if (status === "error") return "failed";
|
||||
return status;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { initialWorkbenchSessionIdFromLocation } from "./workbench-projection";
|
||||
import { cleanupWorkbenchServerStateSessions, createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMessages, selectActiveSession, selectSessionList, selectSessionStatusAuthority, selectTraceAuthorityById, selectTurnStatusAuthority, type WorkbenchServerAction } from "./workbench-server-state";
|
||||
import { cleanupDroppedWorkbenchSessionCaches, trimWorkbenchSessionCache } from "./workbench-session-cache";
|
||||
import { reduceWorkbenchRealtimeEvent, type WorkbenchRealtimeAction } from "./workbench-event-reducer";
|
||||
import { messageHasSealedTerminalResult, messageIsSealedTerminal, traceAuthorityIsSealed } from "./workbench-terminal-authority";
|
||||
import {
|
||||
agentErrorDisplayText,
|
||||
agentErrorFromApiFailure,
|
||||
@@ -1438,18 +1439,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
}));
|
||||
}
|
||||
|
||||
function messageHasSealedTerminalResult(message: ChatMessage | null): boolean {
|
||||
return messageHasTerminalResponse(message);
|
||||
}
|
||||
|
||||
function traceAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null): boolean {
|
||||
return Boolean((turn?.terminal === true || isTerminalMessageStatus(turn?.status)) && messageHasSealedTerminalResult(message));
|
||||
}
|
||||
|
||||
function messageIsSealedTerminal(message: ChatMessage | null): boolean {
|
||||
return messageHasSealedTerminalResult(message);
|
||||
}
|
||||
|
||||
function failTrace(traceId: string, message: string): void {
|
||||
markMessage(traceId, { status: "failed", text: message });
|
||||
chatPending.value = false;
|
||||
|
||||
Reference in New Issue
Block a user