Files
laimiaoai-agent-new/src/agent/confirmations.ts
T
light 6ec9778d45 chore: initial commit
Laimiao agent v2 service: agent orchestration, sessions, billing,
gateway client, skills catalog, auth, config, plus tests.
2026-11-07 10:40:07 +08:00

108 lines
2.9 KiB
TypeScript

import type { Quote } from "../types.js";
export interface ToolConfirmationRequest {
sessionId: string;
toolCallId: string;
toolName: string;
args: unknown;
quote?: Quote;
confirmType?: string;
payload?: unknown;
expiresAt: number;
}
export interface ToolConfirmationDecision {
approved: boolean;
content?: string;
reason?: string;
}
interface PendingConfirmation {
resolve: (decision: ToolConfirmationDecision) => void;
timer: NodeJS.Timeout;
cleanup: () => void;
}
function keyOf(sessionId: string, toolCallId: string): string {
return `${sessionId}:${toolCallId}`;
}
export class ToolConfirmationManager {
private readonly pending = new Map<string, PendingConfirmation>();
constructor(private readonly timeoutMs: number) {}
request(
input: Omit<ToolConfirmationRequest, "expiresAt">,
onRequest: (request: ToolConfirmationRequest) => void,
signal?: AbortSignal,
): Promise<ToolConfirmationDecision> {
if (signal?.aborted) {
return Promise.resolve({ approved: false, reason: "请求已中止" });
}
const key = keyOf(input.sessionId, input.toolCallId);
if (this.pending.has(key)) {
return Promise.resolve({ approved: false, reason: "该工具调用已在等待确认" });
}
const request: ToolConfirmationRequest = {
...input,
expiresAt: Date.now() + this.timeoutMs,
};
return new Promise((resolve) => {
let settled = false;
const finish = (decision: ToolConfirmationDecision): void => {
if (settled) return;
settled = true;
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
this.pending.delete(key);
resolve(decision);
};
const onAbort = (): void => finish({ approved: false, reason: "请求已中止" });
const timer = setTimeout(
() => finish({ approved: false, reason: "等待用户确认超时" }),
this.timeoutMs,
);
timer.unref?.();
this.pending.set(key, {
resolve: finish,
timer,
cleanup: () => finish({ approved: false, reason: "会话已结束" }),
});
signal?.addEventListener("abort", onAbort, { once: true });
try {
onRequest(request);
} catch (error) {
finish({ approved: false, reason: error instanceof Error ? error.message : String(error) });
}
});
}
resolve(
sessionId: string,
toolCallId: string,
decision: boolean | ToolConfirmationDecision,
reason?: string,
): boolean {
const pending = this.pending.get(keyOf(sessionId, toolCallId));
if (!pending) return false;
pending.resolve(typeof decision === "boolean" ? { approved: decision, reason } : decision);
return true;
}
clearSession(sessionId: string): void {
const prefix = `${sessionId}:`;
for (const [key, pending] of this.pending) {
if (key.startsWith(prefix)) pending.cleanup();
}
}
count(): number {
return this.pending.size;
}
}