feat(ai): make agent turn limit configurable

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-07-23 13:57:06 +08:00
1 parent 21e9578f78
commit 9253203d99
21 files changed
+307 -5

No files matched your search

@@ -0,0 +1,64 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { MAX_AGENT_TURNS_DEFAULT, MAX_AGENT_TURNS_MAX, MAX_AGENT_TURNS_MIN, maxAgentTurnsOutOfRange, normalizeMaxAgentTurns } from "@/lib/ai/maxAgentTurns";
const settingsDialogSource = readFileSync(new URL("../../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
describe("maxAgentTurnsOutOfRange", () => {
it("accepts values within [min, max]", () => {
expect(maxAgentTurnsOutOfRange(30)).toBe(false);
expect(maxAgentTurnsOutOfRange(MAX_AGENT_TURNS_MIN)).toBe(false);
expect(maxAgentTurnsOutOfRange(MAX_AGENT_TURNS_MAX)).toBe(false);
});
it("rejects values outside [min, max]", () => {
expect(maxAgentTurnsOutOfRange(MAX_AGENT_TURNS_MIN - 1)).toBe(true);
expect(maxAgentTurnsOutOfRange(MAX_AGENT_TURNS_MAX + 1)).toBe(true);
});
it("leaves loading state to the dialog instead of treating it as a range error", () => {
expect(maxAgentTurnsOutOfRange(undefined)).toBe(false);
});
it("flags +Infinity as out of range (regression: a bare Number.isFinite guard short-circuits this to false)", () => {
// A user can type "1e400" into <input type="number">, which the browser accepts
// and Number() parses to Infinity. The check must not short-circuit on it.
expect(maxAgentTurnsOutOfRange(Number.POSITIVE_INFINITY)).toBe(true);
});
});
describe("agent turn limit loading", () => {
it("starts loading independently and blocks saves until the persisted value arrives", () => {
const aiTabStart = settingsDialogSource.indexOf('if (tab === "ai") {');
const aiTabBranch = settingsDialogSource.slice(aiTabStart, settingsDialogSource.indexOf('if (tab === "about"', aiTabStart));
expect(aiTabBranch.indexOf("void loadMaxAgentTurnsSetting()")).toBeLessThan(aiTabBranch.indexOf("await promptTemplateStore.ensureLoaded()"));
expect(settingsDialogSource).toContain("if (!maxAgentTurnsLoaded.value) return;");
expect(settingsDialogSource).toContain(':disabled="!maxAgentTurnsLoaded || maxAgentTurnsSaving"');
expect(settingsDialogSource).toContain(':disabled="!maxAgentTurnsLoaded || maxAgentTurnsSaving || maxAgentTurnsOutOfRange(editMaxAgentTurns)"');
});
it("keeps failed loads retryable instead of replacing them with the default", () => {
const loadFunction = settingsDialogSource.slice(settingsDialogSource.indexOf("async function loadMaxAgentTurnsSetting()"), settingsDialogSource.indexOf("async function saveMaxAgentTurnsSetting()"));
expect(loadFunction).toContain("maxAgentTurnsLoadError.value = e?.message || String(e)");
expect(loadFunction).not.toContain("editMaxAgentTurns.value = MAX_AGENT_TURNS_DEFAULT");
expect(settingsDialogSource).toContain('v-if="maxAgentTurnsLoadError"');
});
});
describe("normalizeMaxAgentTurns", () => {
it("preserves and rounds finite values within the supported range", () => {
expect(normalizeMaxAgentTurns(100)).toBe(100);
expect(normalizeMaxAgentTurns(30.6)).toBe(31);
});
it("clamps finite values outside the supported range", () => {
expect(normalizeMaxAgentTurns(MAX_AGENT_TURNS_MIN - 1)).toBe(MAX_AGENT_TURNS_MIN);
expect(normalizeMaxAgentTurns(MAX_AGENT_TURNS_MAX + 1)).toBe(MAX_AGENT_TURNS_MAX);
});
it("uses the default for empty and non-finite input", () => {
expect(normalizeMaxAgentTurns(undefined)).toBe(MAX_AGENT_TURNS_DEFAULT);
expect(normalizeMaxAgentTurns(Number.NaN)).toBe(MAX_AGENT_TURNS_DEFAULT);
expect(normalizeMaxAgentTurns(Number.POSITIVE_INFINITY)).toBe(MAX_AGENT_TURNS_DEFAULT);
});
});
+14
View File
@@ -0,0 +1,14 @@
// Mirrors DEFAULT/MIN/MAX_MAX_AGENT_TURNS in crates/dbx-core/src/agent_loop.rs.
// The backend clamp remains the source of truth for persisted values.
export const MAX_AGENT_TURNS_DEFAULT = 30;
export const MAX_AGENT_TURNS_MIN = 5;
export const MAX_AGENT_TURNS_MAX = 500;
export function maxAgentTurnsOutOfRange(value: number | undefined): boolean {
return typeof value === "number" && (value < MAX_AGENT_TURNS_MIN || value > MAX_AGENT_TURNS_MAX);
}
export function normalizeMaxAgentTurns(value: number | undefined): number {
const rounded = typeof value === "number" && Number.isFinite(value) ? Math.round(value) : MAX_AGENT_TURNS_DEFAULT;
return Math.min(MAX_AGENT_TURNS_MAX, Math.max(MAX_AGENT_TURNS_MIN, rounded));
}
+2
View File
@@ -255,6 +255,8 @@ export const loadDesktopSettings = forward("loadDesktopSettings");
export const saveDesktopSettings = forward("saveDesktopSettings");
export const loadMcpGlobalPolicy = forward("loadMcpGlobalPolicy");
export const saveMcpGlobalPolicy = forward("saveMcpGlobalPolicy");
export const loadMaxAgentTurns = forward("loadMaxAgentTurns");
export const saveMaxAgentTurns = forward("saveMaxAgentTurns");
export const completeAppClose = forward("completeAppClose");
export const requestAppClose = forward("requestAppClose");
export const setDriverStoreDir = forward("setDriverStoreDir");
+13
View File
@@ -1202,6 +1202,19 @@ export async function saveMcpGlobalPolicy(policy: Omit<McpGlobalPolicy, "configu
if (!res.ok) throw new Error(await res.text());
}
export async function loadMaxAgentTurns(): Promise<number> {
return get("/api/app-settings/max-agent-turns");
}
export async function saveMaxAgentTurns(maxAgentTurns: number): Promise<void> {
const res = await fetch(apiUrl("/api/app-settings/max-agent-turns"), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ maxAgentTurns }),
});
if (!res.ok) throw new Error(await res.text());
}
export interface OpenTabsStatePayload {
tabs: unknown[];
activeTabId: string | null;
+8
View File
@@ -464,6 +464,14 @@ export async function saveMcpGlobalPolicy(policy: Omit<McpGlobalPolicy, "configu
return invoke("save_mcp_global_policy", { policy });
}
export async function loadMaxAgentTurns(): Promise<number> {
return invoke("load_max_agent_turns");
}
export async function saveMaxAgentTurns(maxAgentTurns: number): Promise<void> {
return invoke("save_max_agent_turns", { maxAgentTurns });
}
export interface OpenTabsStatePayload {
tabs: unknown[];
activeTabId: string | null;