summaryrefslogtreecommitdiffhomepage
path: root/packages/api/tests
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-02 13:02:08 +0900
committerAdam Malczewski <[email protected]>2026-06-02 13:02:08 +0900
commit2503eba38b7885ab92a9c0e4f082323d1b3a8679 (patch)
treecc9ccc2184510d93c601a98ed08a05fdeb146a6f /packages/api/tests
parent3f629a8469fe483243671e1ca15582a111e96541 (diff)
downloaddispatch-2503eba38b7885ab92a9c0e4f082323d1b3a8679.tar.gz
dispatch-2503eba38b7885ab92a9c0e4f082323d1b3a8679.zip
feat(agents): per-model reasoning effort level
Add a per-model/key reasoning effort setting to agent definitions, surfaced and editable in the Agent Settings page and displayed at a glance in the model selector views. - core: single source of truth for effort levels (REASONING_EFFORTS, DEFAULT_REASONING_EFFORT='high', labels, isReasoningEffort guard); add 'xhigh' level; AgentModelEntry.effort; xhigh budget=24000 for classic-thinking Claude; default floor 'high'. Persist/parse effort in the agent TOML loader. - api: thread effort through the fallback chain with per-model -> per-tab -> default precedence; validate /chat + agentModels effort from the canonical list. - frontend: effort <select> per model row in AgentBuilder; effort badges in ModelSelector (agent + subagent chains); Thinking dropdown sourced from canonical list; per-tab default raised to 'high'. - tests: +15 (loader round-trip, agent xhigh budget, canonical list + guard, api precedence, route validation).
Diffstat (limited to 'packages/api/tests')
-rw-r--r--packages/api/tests/agent-manager.test.ts64
-rw-r--r--packages/api/tests/routes.test.ts33
2 files changed, 96 insertions, 1 deletions
diff --git a/packages/api/tests/agent-manager.test.ts b/packages/api/tests/agent-manager.test.ts
index b9b4510..ffd87b5 100644
--- a/packages/api/tests/agent-manager.test.ts
+++ b/packages/api/tests/agent-manager.test.ts
@@ -80,6 +80,13 @@ function resetConstructedAgents(): void {
constructedAgents.length = 0;
}
+// Capture the per-call `run()` options (notably reasoningEffort) so tests can
+// assert the per-model → per-tab → default effort resolution.
+const capturedRunOptions: Array<{ reasoningEffort?: string } | undefined> = [];
+function resetCapturedRunOptions(): void {
+ capturedRunOptions.length = 0;
+}
+
// Configurable settings store so tests can toggle tool permissions
// (perm_send_to_tab / perm_read_tab / ...) and assert which tools the
// constructed Agent receives. Defaults to empty (getSetting → null).
@@ -135,7 +142,7 @@ vi.mock("@dispatch/core", () => ({
constructor(config: { tools?: Array<{ name: string }> }) {
this.toolNames = (config?.tools ?? []).map((t) => t.name);
}
- async *run(message: string): AsyncGenerator<unknown> {
+ async *run(message: string, options?: { reasoningEffort?: string }): AsyncGenerator<unknown> {
// Snapshot the post-construction pre-populated message list
// the first thing `run()` does, before the real `Agent.run`
// would push the current user message at line 546. Tests
@@ -144,6 +151,7 @@ vi.mock("@dispatch/core", () => ({
initialMessages: [...this.messages],
toolNames: [...this.toolNames],
});
+ capturedRunOptions.push(options);
if (runImpl) {
for await (const ev of runImpl(message)) yield ev;
return;
@@ -345,6 +353,11 @@ vi.mock("@dispatch/core", () => ({
getSetting(key: string) {
return fakeSettings.get(key) ?? null;
},
+ isReasoningEffort(value: unknown) {
+ return (
+ typeof value === "string" && ["none", "low", "medium", "high", "xhigh", "max"].includes(value)
+ );
+ },
appendChunks() {
return [];
},
@@ -402,6 +415,7 @@ describe("AgentManager", () => {
beforeEach(() => {
resetFakeMessages();
resetConstructedAgents();
+ resetCapturedRunOptions();
resetFakeTabs();
resetFakeSettings();
setRunImpl(null);
@@ -519,6 +533,54 @@ describe("AgentManager", () => {
expect(listener2).toHaveBeenCalled();
});
+ // ─── per-model reasoning effort precedence ───────────────────────
+
+ describe("reasoning effort precedence (per-model → per-tab → default)", () => {
+ it("uses the per-model effort over the per-tab selector for that fallback entry", async () => {
+ const manager = new AgentManager();
+ // Agent definition supplies a fallback chain where each entry has its
+ // own configured effort; the per-tab selector ("low") must NOT win.
+ await manager.processMessage(
+ "tab-effort-permodel",
+ "go",
+ "key-a",
+ "model-a",
+ "low",
+ undefined,
+ [{ key_id: "key-a", model_id: "model-a", effort: "xhigh" }],
+ );
+ expect(capturedRunOptions.at(-1)?.reasoningEffort).toBe("xhigh");
+ });
+
+ it("falls back to the per-tab selector when the model entry has no effort", async () => {
+ const manager = new AgentManager();
+ await manager.processMessage(
+ "tab-effort-tab",
+ "go",
+ "key-a",
+ "model-a",
+ "medium",
+ undefined,
+ [{ key_id: "key-a", model_id: "model-a" }],
+ );
+ expect(capturedRunOptions.at(-1)?.reasoningEffort).toBe("medium");
+ });
+
+ it("passes no effort (Agent applies its default) when neither is set", async () => {
+ const manager = new AgentManager();
+ await manager.processMessage(
+ "tab-effort-default",
+ "go",
+ "key-a",
+ "model-a",
+ undefined,
+ undefined,
+ [{ key_id: "key-a", model_id: "model-a" }],
+ );
+ expect(capturedRunOptions.at(-1)?.reasoningEffort).toBeUndefined();
+ });
+ });
+
// ─── v6 reasoning-end tests ───────────────────────────────────────
it("reasoning-end event is broadcast to WS listeners", async () => {
diff --git a/packages/api/tests/routes.test.ts b/packages/api/tests/routes.test.ts
index c768cee..3bf446d 100644
--- a/packages/api/tests/routes.test.ts
+++ b/packages/api/tests/routes.test.ts
@@ -169,6 +169,11 @@ vi.mock("@dispatch/core", () => ({
getTab() {
return null;
},
+ isReasoningEffort(value: unknown) {
+ return (
+ typeof value === "string" && ["none", "low", "medium", "high", "xhigh", "max"].includes(value)
+ );
+ },
listOpenTabs() {
return [];
},
@@ -353,6 +358,34 @@ describe("POST /chat", () => {
expect(body).toEqual({ status: "ok" });
});
+ it("accepts xhigh as a valid reasoningEffort", async () => {
+ const res = await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ tabId: "tab-xhigh",
+ message: "hello",
+ reasoningEffort: "xhigh",
+ }),
+ });
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual({ status: "ok" });
+ });
+
+ it("tolerates an invalid agentModels effort (sanitized, not rejected)", async () => {
+ const res = await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ tabId: "tab-badeffort",
+ message: "hello",
+ agentModels: [{ key_id: "k", model_id: "m", effort: "turbo" }],
+ }),
+ });
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual({ status: "ok" });
+ });
+
it("returns 400 with empty message", async () => {
const res = await app.request("/chat", {
method: "POST",