File size: 2,571 Bytes
e249c6d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | import type * as Loopback from "../../gateway/mcp-http.loopback-runtime.js";
import { resolveQuestionTimeoutMs } from "../tools/ask-user-tool-normalization.js";
type McpTerminalOutcome = Loopback.McpLoopbackToolCallTerminalOutcome;
export type CliToolTerminalOutcome = McpTerminalOutcome | { outcome: "completed" };
export type CliLoopbackCall = {
admitted: Loopback.McpLoopbackToolCallStart;
current: Loopback.McpLoopbackToolCallStart;
boundToolCallId?: string;
outcome?: CliToolTerminalOutcome;
ambiguous: boolean;
ambiguityGroup?: CliLoopbackAmbiguityGroup;
};
export type CliLoopbackAmbiguityGroup = {
calls: Set<CliLoopbackCall>;
activeToolCallIds: Set<string>;
};
export type ActiveCliTool = Loopback.McpLoopbackToolCallStart & {
loopbackCall?: CliLoopbackCall;
loopbackAmbiguous: boolean;
ambiguityGroup?: CliLoopbackAmbiguityGroup;
};
export function createAskUserDeadlineTracking(
activeTools: ReadonlyMap<string, ActiveCliTool>,
isOverflowed: () => boolean,
) {
const deadlines = new WeakMap<CliLoopbackCall, number>();
const state: { listeners: Set<() => void>; selected: number | undefined } = {
listeners: new Set(),
selected: undefined,
};
const selectDeadline = () => {
if (isOverflowed()) {
return undefined;
}
let earliest = Number.POSITIVE_INFINITY;
for (const { loopbackCall: call, loopbackAmbiguous } of activeTools.values()) {
if (!call || loopbackAmbiguous || call.ambiguous) {
continue;
}
earliest = Math.min(earliest, deadlines.get(call) ?? Number.POSITIVE_INFINITY);
}
return Number.isFinite(earliest) ? earliest : undefined;
};
const refresh = () => {
const nextDeadline = selectDeadline();
if (nextDeadline === state.selected) {
return;
}
state.selected = nextDeadline;
state.listeners.forEach((listener) => listener());
};
const clear = (call: CliLoopbackCall) => {
deadlines.delete(call);
refresh();
};
const update = (call: CliLoopbackCall, toolName: string, args: Record<string, unknown>) => {
deadlines.delete(call);
if (toolName === "ask_user") {
try {
deadlines.set(call, Date.now() + resolveQuestionTimeoutMs(args.timeoutSeconds));
} catch {
// Invalid timeout input must not arm a watchdog deadline.
}
}
refresh();
};
const onChange = (listener: () => void) => {
state.listeners.add(listener);
return () => void state.listeners.delete(listener);
};
return { clear, update, refresh, get: () => state.selected, onChange };
}
|