142 lines
4.1 KiB
TypeScript
142 lines
4.1 KiB
TypeScript
import { MUTATION_TOOLS } from "./assistant-tools.js";
|
|
import type { PendingAssistantApproval } from "./assistant-approvals.js";
|
|
|
|
export const ASSISTANT_CONFIRMATION_PREFIX = "CONFIRMATION_REQUIRED:";
|
|
|
|
export type ChatMessage = { role: "user" | "assistant"; content: string };
|
|
|
|
export function parseToolArguments(args: string): Record<string, unknown> {
|
|
try {
|
|
const parsed = JSON.parse(args) as unknown;
|
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
? parsed as Record<string, unknown>
|
|
: {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export function formatApprovalValue(value: unknown): string {
|
|
if (typeof value === "string") {
|
|
return value.length > 48 ? `${value.slice(0, 45)}...` : value;
|
|
}
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
return String(value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
if (value.length === 0) return "[]";
|
|
return `[${value.slice(0, 3).map((item) => formatApprovalValue(item)).join(", ")}${value.length > 3 ? ", ..." : ""}]`;
|
|
}
|
|
if (value && typeof value === "object") {
|
|
return "{...}";
|
|
}
|
|
return "null";
|
|
}
|
|
|
|
export function buildApprovalSummary(toolName: string, toolArguments: string): string {
|
|
const params = parseToolArguments(toolArguments);
|
|
const details = Object.entries(params)
|
|
.filter(([, value]) => value !== undefined && value !== null && value !== "")
|
|
.slice(0, 4)
|
|
.map(([key, value]) => `${key}=${formatApprovalValue(value)}`)
|
|
.join(", ");
|
|
|
|
const action = toolName.replace(/_/g, " ");
|
|
return details ? `${action} (${details})` : action;
|
|
}
|
|
|
|
export function isAffirmativeConfirmationReply(content: string): boolean {
|
|
const normalized = content.trim().toLowerCase();
|
|
if (!normalized) return false;
|
|
|
|
const exactMatches = new Set([
|
|
"ja",
|
|
"yes",
|
|
"y",
|
|
"ok",
|
|
"okay",
|
|
"okey",
|
|
"mach das",
|
|
"bitte machen",
|
|
"bitte ausführen",
|
|
"bitte ausfuehren",
|
|
"ausführen",
|
|
"ausfuehren",
|
|
"bestätigt",
|
|
"bestaetigt",
|
|
"bestätigen",
|
|
"bestaetigen",
|
|
"confirm",
|
|
"confirmed",
|
|
"do it",
|
|
"go ahead",
|
|
"proceed",
|
|
]);
|
|
if (exactMatches.has(normalized)) return true;
|
|
|
|
const affirmativePatterns = [
|
|
/^(ja|yes|ok|okay)\b/,
|
|
/\b(mach|make|do|führ|fuehr|execute|run)\b.*\b(das|it|bitte|jetzt)\b/,
|
|
/\b(bit(?:te)?|please)\b.*\b(ausführen|ausfuehren|execute|run|machen|do)\b/,
|
|
/\b(bestätig|bestaetig|confirm)\w*\b/,
|
|
/\b(go ahead|proceed)\b/,
|
|
];
|
|
return affirmativePatterns.some((pattern) => pattern.test(normalized));
|
|
}
|
|
|
|
export function isCancellationReply(content: string): boolean {
|
|
const normalized = content.trim().toLowerCase();
|
|
if (!normalized) return false;
|
|
|
|
const exactMatches = new Set([
|
|
"nein",
|
|
"no",
|
|
"abbrechen",
|
|
"cancel",
|
|
"stopp",
|
|
"stop",
|
|
"doch nicht",
|
|
"nicht ausführen",
|
|
"nicht ausfuehren",
|
|
]);
|
|
if (exactMatches.has(normalized)) return true;
|
|
|
|
return [
|
|
/\b(nein|no|cancel|abbrechen|stop|stopp)\b/,
|
|
/\b(doch nicht|nicht ausführen|nicht ausfuehren)\b/,
|
|
].some((pattern) => pattern.test(normalized));
|
|
}
|
|
|
|
export function hasPendingAssistantConfirmation(messages: ChatMessage[]): boolean {
|
|
if (messages.length < 2) return false;
|
|
const lastMessage = messages[messages.length - 1];
|
|
if (!lastMessage || lastMessage.role !== "user") return false;
|
|
|
|
for (let index = messages.length - 2; index >= 0; index -= 1) {
|
|
const message = messages[index];
|
|
if (!message) continue;
|
|
if (message.role === "assistant") {
|
|
return message.content.trimStart().startsWith(ASSISTANT_CONFIRMATION_PREFIX);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function canExecuteMutationTool(
|
|
messages: ChatMessage[],
|
|
toolName: string,
|
|
pendingApproval?: PendingAssistantApproval | null,
|
|
): boolean {
|
|
if (!MUTATION_TOOLS.has(toolName)) return true;
|
|
const lastMessage = messages[messages.length - 1];
|
|
if (!lastMessage || lastMessage.role !== "user") return false;
|
|
if (!isAffirmativeConfirmationReply(lastMessage.content)) return false;
|
|
|
|
if (pendingApproval) {
|
|
return pendingApproval.toolName === toolName && pendingApproval.expiresAt > Date.now();
|
|
}
|
|
|
|
return hasPendingAssistantConfirmation(messages);
|
|
}
|