feat(ai): make agent turn limit configurable
This commit is contained in:
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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user