chore: initial commit
Laimiao agent v2 service: agent orchestration, sessions, billing, gateway client, skills catalog, auth, config, plus tests.
This commit is contained in:
commit
6ec9778d45
53 files changed
+12340
No files matched your search
@@ -0,0 +1,107 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user