tabby/clients/tabby-agent/src/CompletionDebounce.ts

116 lines
3.8 KiB
TypeScript

import type { CompletionRequest, AbortSignalOption } from "./Agent";
import type { AgentConfig } from "./AgentConfig";
import { rootLogger } from "./logger";
import { splitLines } from "./utils";
function clamp(min: number, max: number, value: number): number {
return Math.max(min, Math.min(max, value));
}
export class CompletionDebounce {
private readonly logger = rootLogger.child({ component: "CompletionDebounce" });
private lastCalledTimeStamp = 0;
private baseInterval = 200; // ms
private calledIntervalHistory: number[] = [];
private options = {
baseIntervalSlideWindowAvg: {
minSize: 20,
maxSize: 100,
min: 100,
max: 400,
},
adaptiveRate: {
min: 1.5,
max: 3.0,
},
contextScoreWeights: {
triggerCharacter: 0.5,
noSuffixInCurrentLine: 0.4,
noSuffix: 0.1,
},
requestDelay: {
min: 100, // ms
max: 1000,
},
};
async debounce(
context: {
request: CompletionRequest;
config: AgentConfig["completion"]["debounce"];
responseTime: number;
},
options?: AbortSignalOption,
): Promise<void> {
const { request, config, responseTime } = context;
if (request.manually) {
return this.sleep(0, options);
}
if (config.mode === "fixed") {
return this.sleep(config.interval, options);
}
const now = Date.now();
this.updateBaseInterval(now - this.lastCalledTimeStamp);
this.lastCalledTimeStamp = now;
const contextScore = this.calcContextScore(request);
const adaptiveRate =
this.options.adaptiveRate.max - (this.options.adaptiveRate.max - this.options.adaptiveRate.min) * contextScore;
const expectedLatency = adaptiveRate * this.baseInterval;
const delay = clamp(this.options.requestDelay.min, this.options.requestDelay.max, expectedLatency - responseTime);
return this.sleep(delay, options);
}
private async sleep(delay: number, options?: AbortSignalOption): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, Math.min(delay, 0x7fffffff));
if (options?.signal) {
if (options.signal.aborted) {
clearTimeout(timer);
reject(options.signal.reason);
} else {
options.signal.addEventListener("abort", () => {
clearTimeout(timer);
reject(options.signal.reason);
});
}
}
});
}
private updateBaseInterval(interval: number) {
if (interval > this.options.baseIntervalSlideWindowAvg.max) {
return;
}
this.calledIntervalHistory.push(interval);
if (this.calledIntervalHistory.length > this.options.baseIntervalSlideWindowAvg.maxSize) {
this.calledIntervalHistory.shift();
}
if (this.calledIntervalHistory.length > this.options.baseIntervalSlideWindowAvg.minSize) {
const avg = this.calledIntervalHistory.reduce((a, b) => a + b, 0) / this.calledIntervalHistory.length;
this.baseInterval = clamp(
this.options.baseIntervalSlideWindowAvg.min,
this.options.baseIntervalSlideWindowAvg.max,
avg,
);
}
}
// return score in [0, 1], 1 means the context has a high chance to accept the completion
private calcContextScore(request: CompletionRequest): number {
let score = 0;
const weights = this.options.contextScoreWeights;
const triggerCharacter = request.text[request.position - 1] ?? "";
score += triggerCharacter.match(/^\W*$/) ? weights.triggerCharacter : 0;
const suffix = request.text.slice(request.position) ?? "";
const currentLineInSuffix = splitLines(suffix)[0] ?? "";
score += currentLineInSuffix.match(/^\W*$/) ? weights.noSuffixInCurrentLine : 0;
score += suffix.match(/^\W*$/) ? weights.noSuffix : 0;
score = clamp(0, 1, score);
return score;
}
}