fix(chat): wire question and todo cards

This commit is contained in:
2026-06-08 18:10:28 +08:00
parent 2691f42581
commit b23cb6acdd
9 changed files with 1713 additions and 10 deletions
+227
View File
@@ -8,6 +8,53 @@ export type AgentModel =
export type PermissionReply = "once" | "always" | "reject";
export type AgentApprovalMode = "request" | "always";
export type AgentQuestionStatus =
| "pending"
| "submitting"
| "answered"
| "rejected"
| "error";
export type AgentQuestionRequest = {
requestId: string;
sessionId: string;
questions: Array<{
header: string;
question: string;
options: Array<{
label: string;
description: string;
}>;
multiple?: boolean;
custom?: boolean;
}>;
tool?: {
messageID: string;
callID: string;
};
createdAt: number;
repliedAt?: number;
status: AgentQuestionStatus;
answers?: string[][];
error?: string;
};
export type AgentTodoItem = {
id: string;
content: string;
status: "pending" | "in_progress" | "completed" | "cancelled";
priority?: "low" | "medium" | "high";
createdAt?: number;
updatedAt?: number;
};
export type AgentTodoUpdate = {
sessionId: string;
messageId?: string;
todos: AgentTodoItem[];
createdAt: number;
};
export type StreamEvent =
| {
type: "state";
@@ -64,6 +111,28 @@ export type StreamEvent =
sessionId: string;
requestId: string;
reply: PermissionReply;
}
| {
type: "question_request";
sessionId: string;
requestId: string;
questions: AgentQuestionRequest["questions"];
tool?: AgentQuestionRequest["tool"];
createdAt: number;
}
| {
type: "question_response";
sessionId: string;
requestId: string;
answers?: string[][];
rejected?: boolean;
}
| {
type: "todo_update";
sessionId: string;
messageId?: string;
todos: AgentTodoItem[];
createdAt: number;
};
type StreamOptions = {
@@ -125,6 +194,80 @@ const resolveToolParams = (
return isObjectRecord(params) ? params : {};
};
const normalizeQuestionList = (value: unknown): AgentQuestionRequest["questions"] => {
if (!Array.isArray(value)) return [];
return value
.filter(isObjectRecord)
.map((question) => ({
header: typeof question.header === "string" ? question.header : "",
question: typeof question.question === "string" ? question.question : "",
options: Array.isArray(question.options)
? question.options.filter(isObjectRecord).map((option) => ({
label: typeof option.label === "string" ? option.label : "",
description:
typeof option.description === "string" ? option.description : "",
}))
: [],
multiple: typeof question.multiple === "boolean" ? question.multiple : undefined,
custom: typeof question.custom === "boolean" ? question.custom : undefined,
}));
};
const normalizeAnswers = (value: unknown): string[][] | undefined => {
if (!Array.isArray(value)) return undefined;
return value.map((answer) =>
Array.isArray(answer)
? answer.filter((item): item is string => typeof item === "string")
: [],
);
};
const normalizeQuestionTool = (value: unknown): AgentQuestionRequest["tool"] => {
if (!isObjectRecord(value)) return undefined;
const messageID =
typeof value.messageID === "string"
? value.messageID
: typeof value.message_id === "string"
? value.message_id
: undefined;
const callID =
typeof value.callID === "string"
? value.callID
: typeof value.call_id === "string"
? value.call_id
: undefined;
return messageID && callID ? { messageID, callID } : undefined;
};
const normalizeTodoStatus = (value: unknown): AgentTodoItem["status"] => {
if (value === "in_progress" || value === "completed" || value === "cancelled") {
return value;
}
return "pending";
};
const normalizeTodoPriority = (value: unknown): AgentTodoItem["priority"] => {
if (value === "low" || value === "medium" || value === "high") {
return value;
}
return undefined;
};
const normalizeTodos = (value: unknown): AgentTodoItem[] => {
if (!Array.isArray(value)) return [];
return value.filter(isObjectRecord).map((todo, index) => ({
id:
typeof todo.id === "string" && todo.id.trim()
? todo.id
: `todo-${index}`,
content: typeof todo.content === "string" ? todo.content : "",
status: normalizeTodoStatus(todo.status),
priority: normalizeTodoPriority(todo.priority),
createdAt: typeof todo.created_at === "number" ? todo.created_at : undefined,
updatedAt: typeof todo.updated_at === "number" ? todo.updated_at : undefined,
}));
};
const emitParsedStreamEvent = (
event: string,
data: string,
@@ -158,6 +301,11 @@ const emitParsedStreamEvent = (
always?: unknown;
created_at?: number;
reply?: PermissionReply;
questions?: unknown;
answers?: unknown;
rejected?: boolean;
message_id?: string;
todos?: unknown;
};
if (event === "state") {
onEvent({
@@ -244,6 +392,31 @@ const emitParsedStreamEvent = (
requestId: parsed.request_id ?? "",
reply: parsed.reply ?? "reject",
});
} else if (event === "question_request") {
onEvent({
type: "question_request",
sessionId: parsed.session_id ?? "",
requestId: parsed.request_id ?? "",
questions: normalizeQuestionList(parsed.questions),
tool: normalizeQuestionTool(parsed.tool),
createdAt: parsed.created_at ?? Date.now(),
});
} else if (event === "question_response") {
onEvent({
type: "question_response",
sessionId: parsed.session_id ?? "",
requestId: parsed.request_id ?? "",
answers: normalizeAnswers(parsed.answers),
rejected: parsed.rejected === true,
});
} else if (event === "todo_update") {
onEvent({
type: "todo_update",
sessionId: parsed.session_id ?? "",
messageId: parsed.message_id,
todos: normalizeTodos(parsed.todos),
createdAt: parsed.created_at ?? Date.now(),
});
}
} catch {
onEvent({
@@ -443,6 +616,60 @@ export const replyAgentPermission = async (
}
};
export const replyAgentQuestion = async (
sessionId: string,
requestId: string,
answers: string[][],
) => {
const response = await apiFetch(
`${config.AGENT_URL}/api/v1/agent/chat/question/${encodeURIComponent(requestId)}/reply`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
session_id: sessionId,
answers,
}),
projectHeaderMode: "include",
userHeaderMode: "include",
skipAuthRedirect: true,
},
);
if (!response.ok) {
const detail = await response.text();
throw new Error(detail || `question reply failed: ${response.status}`);
}
};
export const rejectAgentQuestion = async (
sessionId: string,
requestId: string,
) => {
const response = await apiFetch(
`${config.AGENT_URL}/api/v1/agent/chat/question/${encodeURIComponent(requestId)}/reject`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
session_id: sessionId,
}),
projectHeaderMode: "include",
userHeaderMode: "include",
skipAuthRedirect: true,
},
);
if (!response.ok) {
const detail = await response.text();
throw new Error(detail || `question reject failed: ${response.status}`);
}
};
export const forkAgentChat = async (sessionId: string | undefined, keepMessageCount: number) => {
const response = await apiFetch(`${config.AGENT_URL}/api/v1/agent/chat/fork`, {
method: "POST",