Laimiao agent v2 service: agent orchestration, sessions, billing, gateway client, skills catalog, auth, config, plus tests.
108 lines
2.9 KiB
TypeScript
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;
|
|
}
|
|
}
|