summaryrefslogtreecommitdiffhomepage
path: root/packages/api/src/app.ts
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/src/app.ts
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/src/app.ts')
-rw-r--r--packages/api/src/app.ts39
1 files changed, 32 insertions, 7 deletions
diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts
index 0dabb0d..84afd2a 100644
--- a/packages/api/src/app.ts
+++ b/packages/api/src/app.ts
@@ -1,4 +1,9 @@
-import { getTab, NotificationDispatcher } from "@dispatch/core";
+import {
+ type AgentModelEntry,
+ getTab,
+ isReasoningEffort,
+ NotificationDispatcher,
+} from "@dispatch/core";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { AgentManager } from "./agent-manager.js";
@@ -10,6 +15,28 @@ import { notificationsRoutes } from "./routes/notifications.js";
import { skillsRoutes } from "./routes/skills.js";
import { tabsRoutes } from "./routes/tabs.js";
+/**
+ * Validate and normalise the `agentModels` fallback chain coming from the
+ * frontend. Each entry must carry string `key_id`/`model_id`; an `effort` is
+ * kept only when it's a recognised level (otherwise dropped so the per-tab /
+ * default effort applies). Returns `undefined` when the input isn't an array.
+ */
+function sanitizeAgentModels(raw: unknown): AgentModelEntry[] | undefined {
+ if (!Array.isArray(raw)) return undefined;
+ const out: AgentModelEntry[] = [];
+ for (const m of raw) {
+ if (!m || typeof m !== "object") continue;
+ const entry = m as Record<string, unknown>;
+ if (typeof entry.key_id !== "string" || typeof entry.model_id !== "string") continue;
+ out.push({
+ key_id: entry.key_id,
+ model_id: entry.model_id,
+ ...(isReasoningEffort(entry.effort) ? { effort: entry.effort } : {}),
+ });
+ }
+ return out;
+}
+
export const permissionManager = new PermissionManager();
export const agentManager = new AgentManager(permissionManager);
@@ -86,15 +113,13 @@ app.post("/chat", async (c) => {
const keyId = typeof body.keyId === "string" ? body.keyId : undefined;
const modelId = typeof body.modelId === "string" ? body.modelId : undefined;
- const agentModels = Array.isArray(body.agentModels) ? body.agentModels : undefined;
+ const agentModels = sanitizeAgentModels(body.agentModels);
const workingDirectory =
typeof body.workingDirectory === "string" ? body.workingDirectory : undefined;
const queueId = typeof body.queueId === "string" ? body.queueId : undefined;
- const validEfforts = ["none", "low", "medium", "high", "max"];
- const reasoningEffort =
- typeof body.reasoningEffort === "string" && validEfforts.includes(body.reasoningEffort)
- ? (body.reasoningEffort as "none" | "low" | "medium" | "high" | "max")
- : undefined;
+ const reasoningEffort = isReasoningEffort(body.reasoningEffort)
+ ? body.reasoningEffort
+ : undefined;
// Single routing decision (queue if busy, new turn if idle) shared with the
// `send_to_tab` tool via `AgentManager.deliverMessage`. Non-blocking — a