diff options
| author | Adam Malczewski <[email protected]> | 2026-06-02 13:02:08 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-02 13:02:08 +0900 |
| commit | 2503eba38b7885ab92a9c0e4f082323d1b3a8679 (patch) | |
| tree | cc9ccc2184510d93c601a98ed08a05fdeb146a6f /packages/api/src/app.ts | |
| parent | 3f629a8469fe483243671e1ca15582a111e96541 (diff) | |
| download | dispatch-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.ts | 39 |
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 |
