feat: initialize experimental agent repo
This commit is contained in:
@@ -0,0 +1,460 @@
|
||||
import type { Event as OpencodeEvent, Part } from "@opencode-ai/sdk/v2";
|
||||
|
||||
import { logger } from "../logger.js";
|
||||
import { type QuestionAnswers } from "../runtime/opencode.js";
|
||||
|
||||
export type PermissionRequestPayload = {
|
||||
session_id: string;
|
||||
request_id: string;
|
||||
permission: string;
|
||||
patterns: string[];
|
||||
target?: string;
|
||||
always: string[];
|
||||
tool?: {
|
||||
messageID: string;
|
||||
callID: string;
|
||||
};
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
type QuestionOptionPayload = {
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type QuestionInfoPayload = {
|
||||
header: string;
|
||||
question: string;
|
||||
options: QuestionOptionPayload[];
|
||||
multiple?: boolean;
|
||||
custom?: boolean;
|
||||
};
|
||||
|
||||
export type QuestionRequestPayload = {
|
||||
session_id: string;
|
||||
request_id: string;
|
||||
questions: QuestionInfoPayload[];
|
||||
tool?: {
|
||||
messageID: string;
|
||||
callID: string;
|
||||
};
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
export type TodoItemPayload = {
|
||||
id: string;
|
||||
content: string;
|
||||
status: "pending" | "in_progress" | "completed" | "cancelled";
|
||||
priority?: "low" | "medium" | "high";
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
};
|
||||
|
||||
export type TodoUpdatePayload = {
|
||||
session_id: string;
|
||||
message_id?: string;
|
||||
todos: TodoItemPayload[];
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development";
|
||||
|
||||
const toolLabels: Record<string, string> = {
|
||||
memory_manager: "记忆写入",
|
||||
geocode: "地理编码",
|
||||
session_search: "历史会话检索",
|
||||
skill_manager: "流程沉淀",
|
||||
web_search: "网页搜索",
|
||||
locate_features: "地图定位",
|
||||
zoom_to_map: "地图缩放",
|
||||
view_history: "历史数据面板",
|
||||
view_scada: "SCADA 面板",
|
||||
show_chart: "图表渲染",
|
||||
render_junctions: "节点渲染",
|
||||
};
|
||||
|
||||
export const logDevelopmentDebug = (
|
||||
message: string,
|
||||
metadata: Record<string, unknown>,
|
||||
) => {
|
||||
if (!isDevelopmentDebugLoggingEnabled) {
|
||||
return;
|
||||
}
|
||||
logger.info(metadata, message);
|
||||
};
|
||||
|
||||
export const getErrorMessage = (error: {
|
||||
name: string;
|
||||
data?: { message?: string };
|
||||
}) => error.data?.message ?? error.name;
|
||||
|
||||
export const getUnknownErrorMessage = (error: unknown) => {
|
||||
if (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"name" in error &&
|
||||
typeof error.name === "string"
|
||||
) {
|
||||
const maybeData = "data" in error ? error.data : undefined;
|
||||
return getErrorMessage({
|
||||
name: error.name,
|
||||
data:
|
||||
typeof maybeData === "object" && maybeData !== null && "message" in maybeData
|
||||
? { message: typeof maybeData.message === "string" ? maybeData.message : undefined }
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
};
|
||||
|
||||
export const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
export const normalizeToolParams = (value: unknown): Record<string, unknown> => {
|
||||
if (isObjectRecord(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
return isObjectRecord(parsed) ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const extractRequestReason = (params: Record<string, unknown>) => {
|
||||
const candidates = ["reason", "request_reason", "why", "purpose", "rationale"];
|
||||
for (const key of candidates) {
|
||||
const value = params[key];
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim();
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const isSkillEvent = (event: OpencodeEvent) =>
|
||||
event.type.toLowerCase().includes("skill");
|
||||
|
||||
export const extractSkillAuditInfo = (event: OpencodeEvent) => {
|
||||
const payload = isObjectRecord(event.properties)
|
||||
? (event.properties as Record<string, unknown>)
|
||||
: {};
|
||||
const candidateName =
|
||||
typeof payload.skill === "string"
|
||||
? payload.skill
|
||||
: typeof payload.skillName === "string"
|
||||
? payload.skillName
|
||||
: typeof payload.name === "string"
|
||||
? payload.name
|
||||
: event.type;
|
||||
const reason = extractRequestReason(payload);
|
||||
return {
|
||||
name: candidateName,
|
||||
reason,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const hasToolParams = (params: Record<string, unknown>) =>
|
||||
Object.keys(params).length > 0;
|
||||
|
||||
export const isSessionEvent = (event: OpencodeEvent, sessionId: string) =>
|
||||
"properties" in event &&
|
||||
typeof event.properties === "object" &&
|
||||
event.properties !== null &&
|
||||
"sessionID" in event.properties &&
|
||||
event.properties.sessionID === sessionId;
|
||||
|
||||
export const isPermissionAskedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "permission.asked" }> =>
|
||||
event.type === "permission.asked";
|
||||
|
||||
export const isPermissionV2AskedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "permission.v2.asked" }> =>
|
||||
event.type === "permission.v2.asked";
|
||||
|
||||
export const isPermissionRepliedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "permission.replied" }> =>
|
||||
event.type === "permission.replied";
|
||||
|
||||
export const isPermissionV2RepliedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "permission.v2.replied" }> =>
|
||||
event.type === "permission.v2.replied";
|
||||
|
||||
export const isQuestionAskedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "question.asked" }> =>
|
||||
event.type === "question.asked";
|
||||
|
||||
export const isQuestionV2AskedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "question.v2.asked" }> =>
|
||||
event.type === "question.v2.asked";
|
||||
|
||||
export const isQuestionRepliedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "question.replied" }> =>
|
||||
event.type === "question.replied";
|
||||
|
||||
export const isQuestionV2RepliedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "question.v2.replied" }> =>
|
||||
event.type === "question.v2.replied";
|
||||
|
||||
export const isQuestionRejectedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "question.rejected" }> =>
|
||||
event.type === "question.rejected";
|
||||
|
||||
export const isQuestionV2RejectedEvent = (
|
||||
event: OpencodeEvent,
|
||||
): event is Extract<OpencodeEvent, { type: "question.v2.rejected" }> =>
|
||||
event.type === "question.v2.rejected";
|
||||
|
||||
export const buildPermissionDetail = (
|
||||
event: Extract<OpencodeEvent, { type: "permission.asked" }>,
|
||||
) => {
|
||||
const patterns = event.properties.patterns.length
|
||||
? event.properties.patterns.join(", ")
|
||||
: event.properties.permission;
|
||||
return `需要用户确认权限:${event.properties.permission};匹配规则:${patterns}`;
|
||||
};
|
||||
|
||||
export const buildPermissionV2Detail = (
|
||||
event: Extract<OpencodeEvent, { type: "permission.v2.asked" }>,
|
||||
) => {
|
||||
const resources = event.properties.resources.length
|
||||
? event.properties.resources.join(", ")
|
||||
: event.properties.action;
|
||||
return `需要用户确认权限:${event.properties.action};资源:${resources}`;
|
||||
};
|
||||
|
||||
export const normalizeQuestionPayload = (
|
||||
event: Extract<OpencodeEvent, { type: "question.asked" | "question.v2.asked" }>,
|
||||
clientSessionId: string,
|
||||
): QuestionRequestPayload => ({
|
||||
session_id: clientSessionId,
|
||||
request_id: event.properties.id,
|
||||
questions: event.properties.questions.map((question) => ({
|
||||
header: question.header,
|
||||
question: question.question,
|
||||
options: question.options.map((option) => ({
|
||||
label: option.label,
|
||||
description: option.description,
|
||||
})),
|
||||
multiple: question.multiple,
|
||||
custom: question.custom,
|
||||
})),
|
||||
tool: event.properties.tool,
|
||||
created_at: Date.now(),
|
||||
});
|
||||
|
||||
export const normalizeQuestionAnswers = (answers: QuestionAnswers | undefined) =>
|
||||
Array.isArray(answers)
|
||||
? answers.map((answer) =>
|
||||
Array.isArray(answer)
|
||||
? answer.filter((item): item is string => typeof item === "string")
|
||||
: [],
|
||||
)
|
||||
: [];
|
||||
|
||||
const questionToolNames = new Set(["question", "request_user_input"]);
|
||||
|
||||
const normalizeQuestionOptions = (value: unknown): QuestionOptionPayload[] =>
|
||||
Array.isArray(value)
|
||||
? value.filter(isObjectRecord).map((option) => ({
|
||||
label: typeof option.label === "string" ? option.label : "",
|
||||
description:
|
||||
typeof option.description === "string" ? option.description : "",
|
||||
})).filter((option) => option.label.trim().length > 0)
|
||||
: [];
|
||||
|
||||
const normalizeToolQuestionInfo = (value: unknown): QuestionInfoPayload | undefined => {
|
||||
if (!isObjectRecord(value) || typeof value.question !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const question = value.question.trim();
|
||||
if (!question) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
header:
|
||||
typeof value.header === "string" && value.header.trim()
|
||||
? value.header
|
||||
: "补充信息",
|
||||
question,
|
||||
options: normalizeQuestionOptions(value.options),
|
||||
multiple: typeof value.multiple === "boolean" ? value.multiple : undefined,
|
||||
custom: typeof value.custom === "boolean" ? value.custom : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const normalizeQuestionToolPayload = (
|
||||
part: Extract<Part, { type: "tool" }>,
|
||||
params: Record<string, unknown>,
|
||||
clientSessionId: string,
|
||||
): QuestionRequestPayload | undefined => {
|
||||
if (!questionToolNames.has(part.tool)) {
|
||||
return undefined;
|
||||
}
|
||||
const questions = Array.isArray(params.questions)
|
||||
? params.questions
|
||||
.map(normalizeToolQuestionInfo)
|
||||
.filter((question): question is QuestionInfoPayload => Boolean(question))
|
||||
: [];
|
||||
if (questions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
session_id: clientSessionId,
|
||||
request_id: part.callID || part.id,
|
||||
questions,
|
||||
tool: {
|
||||
messageID: part.messageID,
|
||||
callID: part.callID,
|
||||
},
|
||||
created_at: Date.now(),
|
||||
};
|
||||
};
|
||||
|
||||
export const normalizeTodoStatus = (status: string): TodoItemPayload["status"] => {
|
||||
if (status === "in_progress" || status === "completed" || status === "cancelled") {
|
||||
return status;
|
||||
}
|
||||
return "pending";
|
||||
};
|
||||
|
||||
export const normalizeTodoPriority = (
|
||||
priority: string,
|
||||
): TodoItemPayload["priority"] | undefined => {
|
||||
if (priority === "low" || priority === "medium" || priority === "high") {
|
||||
return priority;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const collectTextContent = (parts: Part[]) =>
|
||||
parts
|
||||
.filter((part): part is Extract<Part, { type: "text" }> => part.type === "text")
|
||||
.map((part) => part.text)
|
||||
.join("");
|
||||
|
||||
export const normalizeToolStatus = (status: string) => {
|
||||
if (status === "completed") return "completed";
|
||||
if (status === "error") return "error";
|
||||
return "running";
|
||||
};
|
||||
|
||||
const formatProgressValue = (value: unknown): string => {
|
||||
if (typeof value === "string") {
|
||||
return value.length > 120 ? `${value.slice(0, 117)}...` : value;
|
||||
}
|
||||
if (
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean" ||
|
||||
value === null ||
|
||||
value === undefined
|
||||
) {
|
||||
return String(value);
|
||||
}
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
return serialized.length > 120 ? `${serialized.slice(0, 117)}...` : serialized;
|
||||
} catch {
|
||||
return "[unserializable]";
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeProgressText = (chunks: string[]) =>
|
||||
chunks.join("").replace(/\s+/g, " ").trim();
|
||||
|
||||
const truncateProgressText = (text: string, maxLength: number) =>
|
||||
text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
|
||||
|
||||
const summarizeToolParams = (params: Record<string, unknown>) => {
|
||||
const ignoredKeys = new Set(["reason", "request_reason", "why", "purpose", "rationale"]);
|
||||
const summary = Object.entries(params)
|
||||
.filter(([key]) => !ignoredKeys.has(key))
|
||||
.slice(0, 4)
|
||||
.map(([key, value]) => `${key}=${formatProgressValue(value)}`)
|
||||
.join(", ");
|
||||
|
||||
return summary || "无附加参数";
|
||||
};
|
||||
|
||||
export const buildSessionStatusDetail = (status: { type: string; message?: string }) => {
|
||||
if (status.type === "retry") {
|
||||
return status.message
|
||||
? `模型请求需要重试,原因:${status.message}`
|
||||
: "模型请求正在重试,等待下一次响应。";
|
||||
}
|
||||
if (status.type === "busy") {
|
||||
return status.message
|
||||
? `Agent 正在处理中:${status.message}`
|
||||
: "Agent 正在执行推理、工具调用或结果整理。";
|
||||
}
|
||||
if (status.type === "idle") {
|
||||
return status.message
|
||||
? `Agent 已空闲:${status.message}`
|
||||
: "当前会话暂时没有待处理任务。";
|
||||
}
|
||||
return status.message ? `会话状态更新:${status.message}` : `会话状态更新:${status.type}`;
|
||||
};
|
||||
|
||||
export const buildReasoningProgressDetail = (
|
||||
chunks: string[],
|
||||
ended?: string | number | Date | null,
|
||||
) => {
|
||||
const reasoningText = truncateProgressText(normalizeProgressText(chunks), 800);
|
||||
if (ended) {
|
||||
return reasoningText
|
||||
? `推理过程:${reasoningText}`
|
||||
: "当前推理阶段已完成,Agent 将继续输出答案或进入工具执行。";
|
||||
}
|
||||
return reasoningText
|
||||
? `正在推理:${reasoningText}`
|
||||
: "Agent 正在拆解问题、梳理执行步骤并判断是否需要调用工具。";
|
||||
};
|
||||
|
||||
export const buildToolProgressDetail = (
|
||||
tool: string,
|
||||
status: string,
|
||||
params: Record<string, unknown>,
|
||||
reason: string,
|
||||
error?: string,
|
||||
) => {
|
||||
const toolName = toolLabels[tool] ?? tool;
|
||||
const reasonText = reason ? `;调用原因:${reason}` : "";
|
||||
const paramsText = `;关键参数:${summarizeToolParams(params)}`;
|
||||
|
||||
if (status === "error") {
|
||||
const errorText = error ? `;错误:${error}` : "";
|
||||
return `${toolName} 调用失败${reasonText}${paramsText}${errorText}`;
|
||||
}
|
||||
if (status === "completed") {
|
||||
return `${toolName} 已执行完成${reasonText}${paramsText}`;
|
||||
}
|
||||
if (status === "pending") {
|
||||
return `${toolName} 已进入待执行状态${reasonText}${paramsText}`;
|
||||
}
|
||||
return `${toolName} 正在执行${reasonText}${paramsText}`;
|
||||
};
|
||||
|
||||
export const getToolProgressTitle = (tool: string, status: string) => {
|
||||
const toolName = toolLabels[tool] ?? tool;
|
||||
if (status === "completed") return `${toolName} 已完成`;
|
||||
if (status === "error") return `${toolName} 执行失败`;
|
||||
if (status === "pending") return `准备调用 ${toolName}`;
|
||||
return `正在调用 ${toolName}`;
|
||||
};
|
||||
Reference in New Issue
Block a user