Files
next-tjwater-drainage-frontend/features/agent/session-state.ts
T

371 lines
11 KiB
TypeScript

import type {
AgentChatMessage,
AgentChatProgress,
AgentInteractionToolRef,
AgentPermissionRequest,
AgentPermissionStatus,
AgentQuestionInfo,
AgentQuestionRequest,
AgentTodoItem,
AgentTodoUpdate
} from "./types";
type RecordValue = Record<string, unknown>;
export function updateMessageById(
messages: AgentChatMessage[],
messageId: string,
updater: (message: AgentChatMessage) => AgentChatMessage
) {
return messages.map((message) => (message.id === messageId ? updater(message) : message));
}
export function upsertProgress(progress: AgentChatProgress[] | undefined, data: unknown) {
const payload = asRecord(data);
const id = readString(payload.id) ?? `progress-${Date.now().toString(36)}`;
const now = Date.now();
const index = progress?.findIndex((item) => item.id === id) ?? -1;
const existing = index >= 0 ? progress?.[index] : undefined;
const status = readProgressStatus(payload.status);
const startedAt = readNumber(payload.started_at) ?? readNumber(payload.startedAt) ?? existing?.startedAt;
const endedAt = readNumber(payload.ended_at) ?? readNumber(payload.endedAt);
const elapsedMs = readNumber(payload.elapsed_ms) ?? readNumber(payload.elapsedMs);
const durationMs = readNumber(payload.duration_ms) ?? readNumber(payload.durationMs);
const nextItem: AgentChatProgress = {
id,
phase: readString(payload.phase) ?? "progress",
status,
title: readString(payload.title) ?? "正在处理",
detail: readString(payload.detail),
startedAt,
endedAt,
elapsedMs: status === "running" ? elapsedMs : undefined,
elapsedSnapshotAt: status === "running" && elapsedMs !== undefined ? now : undefined,
durationMs: status === "running" ? undefined : durationMs
};
const next = [...(progress ?? [])];
if (index >= 0) {
next[index] = nextItem;
} else {
next.push(nextItem);
}
return next;
}
export function completeRunningProgress(progress: AgentChatProgress[] | undefined) {
return progress?.map((item) => {
if (item.status !== "running") {
return item;
}
const endedAt = Date.now();
return {
...item,
status: "completed" as const,
endedAt,
elapsedMs: undefined,
elapsedSnapshotAt: undefined,
durationMs:
item.durationMs ??
(item.startedAt !== undefined ? Math.max(0, endedAt - item.startedAt) : item.elapsedMs)
};
});
}
export function toTodoUpdate(data: unknown): AgentTodoUpdate | null {
const payload = asRecord(data);
const sessionId = readString(payload.session_id) ?? readString(payload.sessionId);
const rawTodos = Array.isArray(payload.todos) ? payload.todos : [];
if (!sessionId || rawTodos.length === 0) {
return null;
}
return {
sessionId,
messageId: readString(payload.message_id) ?? readString(payload.messageId),
todos: rawTodos.map(toTodoItem).filter((item): item is AgentTodoItem => Boolean(item)),
createdAt: readNumber(payload.created_at) ?? readNumber(payload.createdAt) ?? Date.now()
};
}
export function cancelRunningTodos(todoUpdate: AgentTodoUpdate | undefined) {
return todoUpdate
? {
...todoUpdate,
todos: todoUpdate.todos.map((todo) =>
todo.status === "pending" || todo.status === "in_progress"
? {
...todo,
status: "cancelled" as const,
updatedAt: Date.now()
}
: todo
)
}
: undefined;
}
export function upsertPermission(
permissions: AgentPermissionRequest[] | undefined,
data: unknown
) {
const payload = asRecord(data);
const requestId = readString(payload.request_id) ?? readString(payload.requestId);
const sessionId = readString(payload.session_id) ?? readString(payload.sessionId);
if (!requestId || !sessionId) {
return permissions;
}
const nextItem: AgentPermissionRequest = {
requestId,
sessionId,
permission: readString(payload.permission) ?? "unknown",
patterns: readStringArray(payload.patterns),
target: readString(payload.target),
always: readStringArray(payload.always),
tool: toToolRef(payload.tool),
createdAt: readNumber(payload.created_at) ?? readNumber(payload.createdAt) ?? Date.now(),
status: "pending"
};
const next = [...(permissions ?? [])];
const index = next.findIndex((permission) => permission.requestId === requestId);
if (index >= 0) {
next[index] = {
...next[index],
...nextItem,
status: next[index].status === "submitting" ? "submitting" : nextItem.status
};
} else {
next.push(nextItem);
}
return next;
}
export function applyPermissionResponse(
permissions: AgentPermissionRequest[] | undefined,
data: unknown
) {
const payload = asRecord(data);
const requestId = readString(payload.request_id) ?? readString(payload.requestId);
if (!requestId) {
return permissions;
}
return permissions?.map((permission) =>
permission.requestId === requestId
? {
...permission,
status: toPermissionStatus(readString(payload.reply)),
repliedAt: Date.now(),
error: undefined
}
: permission
);
}
export function upsertQuestion(questions: AgentQuestionRequest[] | undefined, data: unknown) {
const payload = asRecord(data);
const requestId = readString(payload.request_id) ?? readString(payload.requestId);
const sessionId = readString(payload.session_id) ?? readString(payload.sessionId);
const rawQuestions = Array.isArray(payload.questions) ? payload.questions : [];
if (!requestId || !sessionId || rawQuestions.length === 0) {
return questions;
}
const nextItem: AgentQuestionRequest = {
requestId,
sessionId,
questions: rawQuestions.map(toQuestionInfo).filter((item): item is AgentQuestionInfo => Boolean(item)),
tool: toToolRef(payload.tool),
createdAt: readNumber(payload.created_at) ?? readNumber(payload.createdAt) ?? Date.now(),
status: "pending"
};
const next = [...(questions ?? [])];
const index = next.findIndex((question) => isSameQuestion(question, nextItem));
if (index >= 0) {
next[index] = {
...next[index],
...nextItem,
status: next[index].status === "submitting" ? "submitting" : nextItem.status
};
} else {
next.push(nextItem);
}
return next;
}
export function applyQuestionResponse(questions: AgentQuestionRequest[] | undefined, data: unknown) {
const payload = asRecord(data);
const requestId = readString(payload.request_id) ?? readString(payload.requestId);
if (!requestId) {
return questions;
}
const answers = Array.isArray(payload.answers)
? payload.answers.map((answer) => (Array.isArray(answer) ? answer.map(String) : []))
: undefined;
const rejected = payload.rejected === true;
return questions?.map((question) =>
question.requestId === requestId
? {
...question,
status: rejected ? ("rejected" as const) : ("answered" as const),
answers: answers ?? question.answers,
repliedAt: Date.now(),
error: undefined
}
: question
);
}
export function finalizeAssistantMessageAfterAbort(message: AgentChatMessage): AgentChatMessage {
const progress = completeRunningProgress(message.progress);
const todos = cancelRunningTodos(message.todos);
return {
...message,
content: message.content || "请求已中止。",
progress,
todos,
permissions: message.permissions?.map((permission) =>
permission.status === "pending" || permission.status === "submitting" || permission.status === "error"
? {
...permission,
status: "aborted" as const,
repliedAt: Date.now(),
error: undefined
}
: permission
),
questions: message.questions?.map((question) =>
question.status === "pending" || question.status === "submitting" || question.status === "error"
? {
...question,
status: "rejected" as const,
repliedAt: Date.now(),
error: undefined
}
: question
)
};
}
function toPermissionStatus(reply: string | undefined): AgentPermissionStatus {
if (reply === "always") {
return "approved_always";
}
if (reply === "once") {
return "approved_once";
}
return "rejected";
}
function isSameQuestion(left: AgentQuestionRequest, right: AgentQuestionRequest) {
if (left.requestId === right.requestId) {
return true;
}
return Boolean(left.tool?.callID && right.tool?.callID && left.tool.callID === right.tool.callID);
}
function toQuestionInfo(value: unknown): AgentQuestionInfo | null {
const payload = asRecord(value);
const question = readString(payload.question);
if (!question) {
return null;
}
return {
header: readString(payload.header) ?? "",
question,
options: Array.isArray(payload.options)
? payload.options
.map((option) => {
const item = asRecord(option);
const label = readString(item.label);
return label
? {
label,
description: readString(item.description) ?? ""
}
: null;
})
.filter((item): item is { label: string; description: string } => Boolean(item))
: [],
multiple: typeof payload.multiple === "boolean" ? payload.multiple : undefined,
custom: typeof payload.custom === "boolean" ? payload.custom : undefined
};
}
function toTodoItem(value: unknown): AgentTodoItem | null {
const payload = asRecord(value);
const id = readString(payload.id);
const content = readString(payload.content);
const status = readTodoStatus(payload.status);
if (!id || !content || !status) {
return null;
}
return {
id,
content,
status,
priority: readTodoPriority(payload.priority),
createdAt: readNumber(payload.created_at) ?? readNumber(payload.createdAt),
updatedAt: readNumber(payload.updated_at) ?? readNumber(payload.updatedAt)
};
}
function toToolRef(value: unknown): AgentInteractionToolRef | undefined {
const payload = asRecord(value);
const messageID = readString(payload.messageID);
const callID = readString(payload.callID);
return messageID && callID ? { messageID, callID } : undefined;
}
function readProgressStatus(value: unknown): AgentChatProgress["status"] {
return value === "completed" || value === "error" ? value : "running";
}
function readTodoStatus(value: unknown): AgentTodoItem["status"] | undefined {
if (value === "pending" || value === "in_progress" || value === "completed" || value === "cancelled") {
return value;
}
return undefined;
}
function readTodoPriority(value: unknown): AgentTodoItem["priority"] | undefined {
if (value === "low" || value === "medium" || value === "high") {
return value;
}
return undefined;
}
function asRecord(value: unknown): RecordValue {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as RecordValue)
: {};
}
function readString(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function readNumber(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function readStringArray(value: unknown) {
return Array.isArray(value) ? value.map((item) => String(item).trim()).filter(Boolean) : [];
}