diff --git a/README.md b/README.md index 9a9e442e..6bea674f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Use dreb if you want a coding agent that can run against direct APIs, coding sub ## Why choose dreb? - **Every session, on every device.** The [web dashboard](#web-dashboard) is a first-party browser UI for the same sessions the terminal runs: a fleet overview of all live and past sessions across projects, full chat with steering, live subagent observability, host file access, dreb memory management, and settings — one synchronized state on desktop and mobile. Local-only by default; remote access is Tailscale-gated with device pairing. -- **Model and provider freedom.** Authenticate with API keys or `/login` subscriptions, switch models at runtime with `/model`, scope model sets, tune thinking levels, route built-in providers through proxies, use cloud providers such as Bedrock/Vertex/Azure, or add local/proxy/custom models through [Custom Models](packages/coding-agent/docs/models.md) and [Custom Providers](packages/coding-agent/docs/custom-provider.md). See [Providers](packages/coding-agent/docs/providers.md) for the current setup list. +- **Model and provider freedom.** Authenticate with API keys or `/login` subscriptions, switch models at runtime with `/model`, scope model sets, and tune model-aware thinking levels through `xhigh` plus a model-aware `max` tier. Codex `ultra` is orchestration (`max` plus local multi-agent work), not a raw provider effort. Route built-in providers through proxies, use cloud providers such as Bedrock/Vertex/Azure, or add local/proxy/custom models through [Custom Models](packages/coding-agent/docs/models.md) and [Custom Providers](packages/coding-agent/docs/custom-provider.md). See [Providers](packages/coding-agent/docs/providers.md) for the current setup list. - **A real development workflow.** [mach6](packages/coding-agent/docs/mach6.md) is a built-in issue-to-merge workflow: assess issues, plan work, open draft PRs, implement, push progress, run multi-agent reviews, independently assess findings, fix CI or review items, and publish. Plans, reviews, and progress live on GitHub as shared memory. - **Composable agent building blocks.** [Skills](packages/coding-agent/docs/skills.md) are markdown workflows loaded on demand; [extensions](packages/coding-agent/docs/extensions.md) are TypeScript modules for custom tools, commands, event hooks, UI components, renderers, keybindings, provider registration, permission gates, and workflow automation; [packages](packages/coding-agent/docs/packages.md) bundle skills, extensions, prompts, and themes for npm, git, or local sharing. - **Parallel and specialized agents.** The optional `subagent` tool runs role-matched work in independent child agents using single, parallel, or chain mode. Omitting the agent type selects `Explore`, which retrieves concrete evidence such as files, symbols, documentation, call sites, exact snippets, and explicit data flows; the primary agent retains root-cause diagnosis, requirements interpretation, design, implementation recommendations, planning, synthesis, and final conclusions. Parallel and chain modes do not relax that boundary, while specialized agents continue to perform the broader work in their own definitions. Custom agent definitions can inherit models, record child-session metadata for audit trails, and power workflows such as mach6's specialized reviewers. Per-agent models and per-request thinking remain explicit controls. The built-in [`model-routing-guide` skill](packages/coding-agent/docs/skills.md#model-routing-guide) researches scoped provider/model candidates and local child history, and its `update` mode preserves retained entries while removing stale scope and researching newly added models; the optional global-only [Dispatch Arbiter](packages/coding-agent/docs/agent-models.md#dispatch-arbiter) consumes that guide in a fully headless, tool-less call before every child spawn and may change only agent, scoped canonical model, and supported thinking. Its bounded rolling parent activity follows the title setter and includes useful tool outputs, with existing secret scrubbing applied before inference. It is disabled by default and fails closed—bad configuration, guide, inference, or decisions prevent the child from spawning rather than silently keeping the original route. TUI `/settings` and dashboard Settings expose its enable toggle, exact model, thinking, guide path, and validation/readiness feedback. Typed decisions are persisted and visible in the TUI, JSON/RPC, and dashboard. The same settings surfaces expose `backgroundAgents.maxConcurrentSubagents` (default `4`); `0` starts new parent sessions without the subagent tool and explicitly tells the parent model to perform normally delegated work itself. While background subagents run, a separate guardrail pauses the parent after a few turns, configurable via [`backgroundAgents`](packages/coding-agent/docs/settings.md#background-agents). diff --git a/package-lock.json b/package-lock.json index cef71a75..9a41dd80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dreb", - "version": "2.61.0", + "version": "2.61.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dreb", - "version": "2.61.0", + "version": "2.61.1", "workspaces": [ "packages/*", "packages/coding-agent/examples/extensions/with-deps", @@ -8299,27 +8299,6 @@ "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", "license": "MIT" }, - "node_modules/openai": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", - "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -10955,7 +10934,7 @@ }, "packages/agent": { "name": "@dreb/agent-core", - "version": "2.61.0", + "version": "2.61.1", "license": "MIT", "dependencies": { "@dreb/ai": "*" @@ -10984,7 +10963,7 @@ }, "packages/ai": { "name": "@dreb/ai", - "version": "2.61.0", + "version": "2.61.1", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", @@ -10995,7 +10974,7 @@ "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "chalk": "^5.6.2", - "openai": "6.26.0", + "openai": "6.49.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", "undici": "^7.19.1", @@ -11013,6 +10992,66 @@ "node": "22.x" } }, + "packages/ai/node_modules/@smithy/core": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.2.tgz", + "integrity": "sha512-CUGXpnPkVdjUCbix+83sWLW9VFgQOm44MDOx/ihITJMAnOZKvL8YYIc7DR9pP/tZ8CIRvMiON/TucvygqbHO3w==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/ai/node_modules/@smithy/hash-node": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.5.2.tgz", + "integrity": "sha512-OcD8fGClTkP0BWHVEAgUp1RZyCw8cKfqTPQ+DgrSF5jvR8zKkw2Aud79L4G/1Fu3QKLcsHExxRIPQCcKx7+xkg==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/core": "^3.33.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/ai/node_modules/@smithy/signature-v4": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.2.tgz", + "integrity": "sha512-P7Ki6px6OOrxVtx8K7nLmyx4SlXUW/uTKDdMG44UHefmPGSRMBKe2v+TM59WdLcpUIrBrnuCsIqiM2MbsZjmhw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/ai/node_modules/@smithy/types": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.17.2.tgz", + "integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "packages/ai/node_modules/@types/node": { "version": "24.12.0", "dev": true, @@ -11033,6 +11072,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "packages/ai/node_modules/openai": { + "version": "6.49.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.49.0.tgz", + "integrity": "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "packages/ai/node_modules/undici-types": { "version": "7.16.0", "dev": true, @@ -11040,7 +11109,7 @@ }, "packages/coding-agent": { "name": "@dreb/coding-agent", - "version": "2.61.0", + "version": "2.61.1", "license": "MIT", "dependencies": { "@dreb/agent-core": "*", @@ -11169,7 +11238,7 @@ }, "packages/dashboard": { "name": "@dreb/dashboard", - "version": "2.61.0", + "version": "2.61.1", "license": "MIT", "dependencies": { "@dreb/coding-agent": "*", @@ -11401,7 +11470,7 @@ }, "packages/semantic-search": { "name": "@dreb/semantic-search", - "version": "2.61.0", + "version": "2.61.1", "license": "MIT", "dependencies": { "@huggingface/transformers": "^4.0.1", @@ -11450,7 +11519,7 @@ }, "packages/telegram": { "name": "@dreb/telegram", - "version": "2.61.0", + "version": "2.61.1", "license": "MIT", "dependencies": { "@dreb/coding-agent": "*", @@ -11483,7 +11552,7 @@ }, "packages/tui": { "name": "@dreb/tui", - "version": "2.61.0", + "version": "2.61.1", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", diff --git a/package.json b/package.json index 81b49e27..47c071a0 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "node": "22.x" }, "packageManager": "npm@11.5.1", - "version": "2.61.0", + "version": "2.61.1", "dependencies": { "@dreb/coding-agent": "*", "@mariozechner/jiti": "^2.6.5", diff --git a/packages/agent/README.md b/packages/agent/README.md index 060d2e52..eb27073f 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -150,7 +150,7 @@ const agent = new Agent({ initialState: { systemPrompt: string, model: Model, - thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh", + thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", tools: AgentTool[], messages: AgentMessage[], }, diff --git a/packages/agent/package.json b/packages/agent/package.json index 7658deec..02ebee67 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@dreb/agent-core", - "version": "2.61.0", + "version": "2.61.1", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index a5f87eae..920ca86e 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -9,6 +9,7 @@ import { DEFAULT_MAX_OUTPUT_TOKENS, EventStream, streamSimple, + supportsMax, supportsXhigh, type ToolResultMessage, validateToolArguments, @@ -30,6 +31,7 @@ export type AgentEventSink = (event: AgentEvent) => Promise | void; function getEffectiveThinkingLevel(config: AgentLoopConfig): ThinkingLevel { const requested = config.reasoning ?? "off"; if (!config.model.reasoning) return "off"; + if (requested === "max" && !supportsMax(config.model)) return supportsXhigh(config.model) ? "xhigh" : "high"; return requested === "xhigh" && !supportsXhigh(config.model) ? "high" : requested; } diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 61f92bf7..29189f51 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -263,7 +263,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions { * Thinking/reasoning level for models that support it. * Note: "xhigh" is only supported by OpenAI reasoning models that explicitly advertise extended reasoning (for example GPT-5.2+ and Codex GPT-5.3+). */ -export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; +export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; /** * Extensible interface for custom app messages. diff --git a/packages/ai/README.md b/packages/ai/README.md index d34507bd..bf21f2bc 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -443,7 +443,8 @@ if (model.reasoning) { const response = await completeSimple(model, { messages: [{ role: 'user', content: 'Solve: 2x + 5 = 13' }] }, { - reasoning: 'medium' // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' (xhigh maps to 'max' on Claude Opus 4.6–4.x and Claude 5 families; 'high' on other Anthropic models) + reasoning: 'medium' // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' + // Native normalized 'max' is model-aware (currently GPT-5.6). Claude's established xhigh mapping still emits provider-native 'max'. }); // Access thinking and text blocks diff --git a/packages/ai/package.json b/packages/ai/package.json index 6a3990a4..017343eb 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@dreb/ai", - "version": "2.61.0", + "version": "2.61.1", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", @@ -83,7 +83,7 @@ "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "chalk": "^5.6.2", - "openai": "6.26.0", + "openai": "6.49.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", "undici": "^7.19.1", diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 734c70bf..8e79722e 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -187,6 +187,17 @@ export function isQwen38OrLater(modelId: string): boolean { return qwen.major > 3 || (qwen.major === 3 && qwen.minor >= 8); } +/** + * Check if a model supports the native `max` reasoning tier. + * + * Supported today: + * - GPT-5.6 model families (Sol, Terra, Luna, and the alias) + */ +export function supportsMax(model: Model): boolean { + if (/(?:^|\/)gpt-5\.6(?:$|[-.])/.test(model.id.toLowerCase())) return true; + return false; +} + /** * Check if a model supports xhigh thinking level. * diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index b0240af9..35d5563d 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -415,6 +415,7 @@ function mapThinkingLevelToEffort( case "high": return "high"; case "xhigh": + case "max": return supportsXhigh(model) ? "max" : "high"; default: return "high"; @@ -732,10 +733,11 @@ export function buildAdditionalModelRequestFields( medium: 8192, high: 16384, xhigh: 16384, // Claude doesn't support xhigh, clamp to high + max: 16384, // Normalized max falls back through xhigh to high here }; - // Custom budgets override defaults (xhigh not in ThinkingBudgets, use high) - const level = options.reasoning === "xhigh" ? "high" : options.reasoning; + // Custom budgets do not define xhigh/max, so both use high. + const level = options.reasoning === "xhigh" || options.reasoning === "max" ? "high" : options.reasoning; const budget = options.thinkingBudgets?.[level] ?? defaultBudgets[options.reasoning]; return { diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 805f560f..fee30fbc 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -479,6 +479,7 @@ function mapThinkingLevelToEffort( case "high": return "high"; case "xhigh": + case "max": return supportsXhigh(model) ? "max" : "high"; default: return "high"; diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index df7aed9f..0208ea67 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -1,7 +1,6 @@ import { AzureOpenAI } from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; import { getEnvApiKey } from "../env-api-keys.js"; -import { supportsXhigh } from "../models.js"; import type { Api, AssistantMessage, @@ -13,7 +12,7 @@ import type { } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions, resolveReasoningEffort } from "./simple-options.js"; const DEFAULT_AZURE_API_VERSION = "v1"; const AZURE_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode", "azure-openai-responses"]); @@ -41,7 +40,7 @@ function resolveDeploymentName(model: Model<"azure-openai-responses">, options?: // Azure OpenAI Responses-specific options export interface AzureOpenAIResponsesOptions extends StreamOptions { - reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; + reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; reasoningSummary?: "auto" | "detailed" | "concise" | null; azureApiVersion?: string; azureResourceName?: string; @@ -133,7 +132,7 @@ export const streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-resp } const base = buildBaseOptions(model, options, apiKey); - const reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning); + const reasoningEffort = resolveReasoningEffort(model, options?.reasoning); return streamAzureOpenAIResponses(model, context, { ...base, diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index b30f79dd..dcd279d5 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -964,7 +964,7 @@ export function buildRequest( }; } -type ClampedThinkingLevel = Exclude; +type ClampedThinkingLevel = Exclude; function getDisabledThinkingConfig(modelId: string): ThinkingConfig { // Google docs: Gemini 3.1 Pro cannot disable thinking, and Gemini 3 Flash / Flash-Lite diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index 551e30e4..2be07406 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -463,7 +463,7 @@ function buildParams( return params; } -type ClampedThinkingLevel = Exclude; +type ClampedThinkingLevel = Exclude; function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase()); diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index a6209a90..904fe251 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -398,7 +398,7 @@ function buildParams( return params; } -type ClampedThinkingLevel = Exclude; +type ClampedThinkingLevel = Exclude; function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase()); diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 6ef6bf89..70a1d25f 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -21,7 +21,6 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version } import { getEnvApiKey } from "../env-api-keys.js"; -import { supportsXhigh } from "../models.js"; import type { Api, AssistantMessage, @@ -34,7 +33,7 @@ import type { } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions, resolveReasoningEffort } from "./simple-options.js"; // ============================================================================ // Configuration @@ -67,7 +66,7 @@ const CODEX_RESPONSE_STATUSES = new Set([ // ============================================================================ export interface OpenAICodexResponsesOptions extends StreamOptions { - reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh"; + reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; reasoningSummary?: "auto" | "concise" | "detailed" | "off" | "on" | null; textVerbosity?: "low" | "medium" | "high"; serviceTier?: ResponseCreateParamsStreaming["service_tier"]; @@ -328,7 +327,7 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp } const base = buildBaseOptions(model, options, apiKey); - const reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning); + const reasoningEffort = resolveReasoningEffort(model, options?.reasoning); return streamOpenAICodexResponses(model, context, { ...base, diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 37f361be..bc212be6 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -9,7 +9,7 @@ import type { ChatCompletionToolMessageParam, } from "openai/resources/chat/completions.js"; import { getEnvApiKey } from "../env-api-keys.js"; -import { calculateCost, isQwen38OrLater, supportsXhigh } from "../models.js"; +import { calculateCost, isQwen38OrLater } from "../models.js"; import type { AssistantMessage, Context, @@ -30,7 +30,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; -import { buildBaseOptions, clampReasoning } from "./simple-options.js"; +import { buildBaseOptions, resolveReasoningEffort } from "./simple-options.js"; import { transformMessages } from "./transform-messages.js"; const STRUCTURED_REASONING_FIELDS = new Set(["reasoning_content", "reasoning", "reasoning_text"]); @@ -56,7 +56,7 @@ function hasToolHistory(messages: Message[]): boolean { export interface OpenAICompletionsOptions extends StreamOptions { toolChoice?: "auto" | "none" | "required" | { type: "function"; function: { name: string } }; - reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; + reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; } export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions> = ( @@ -321,7 +321,7 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", } const base = buildBaseOptions(model, options, apiKey); - const reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning); + const reasoningEffort = resolveReasoningEffort(model, options?.reasoning); const toolChoice = (options as OpenAICompletionsOptions | undefined)?.toolChoice; return streamOpenAICompletions(model, context, { @@ -913,6 +913,7 @@ const QWEN38_PLUS_EFFORT_MAP: Record, options?: SimpleStreamOption }; } -export function clampReasoning(effort: ThinkingLevel | undefined): Exclude | undefined { - return effort === "xhigh" ? "high" : effort; +export function clampReasoning(effort: ThinkingLevel | undefined): Exclude | undefined { + return effort === "xhigh" || effort === "max" ? "high" : effort; +} + +export function resolveReasoningEffort( + model: Model, + effort: ThinkingLevel | undefined, +): ThinkingLevel | undefined { + if (effort === "max" && !supportsMax(model)) return supportsXhigh(model) ? "xhigh" : "high"; + if (effort === "xhigh" && !supportsXhigh(model)) return "high"; + return effort; } export function adjustMaxTokensForThinking( diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 1d658244..76a01b1f 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -42,7 +42,7 @@ export type KnownProvider = | "kimi-coding-oauth"; export type Provider = KnownProvider | string; -export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; +export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; /** Token budgets for each thinking level (token-based providers only) */ export interface ThinkingBudgets { diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 011dab2d..efac14d5 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -1118,104 +1118,109 @@ describe("openai-codex streaming", () => { }); }); - it.each(["gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"])( - "clamps %s minimal reasoning effort to low", - async (modelId) => { - const tempDir = mkdtempSync(join(tmpdir(), "dreb-codex-stream-")); - process.env.DREB_CODING_AGENT_DIR = tempDir; - - const payload = Buffer.from( - JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), - "utf8", - ).toString("base64"); - const token = `aaa.${payload}.bbb`; - - const sse = `${[ - `data: ${JSON.stringify({ - type: "response.output_item.added", - item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] }, - })}`, - `data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`, - `data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Hello" })}`, - `data: ${JSON.stringify({ - type: "response.output_item.done", - item: { - type: "message", - id: "msg_1", - role: "assistant", - status: "completed", - content: [{ type: "output_text", text: "Hello" }], - }, - })}`, - `data: ${JSON.stringify({ - type: "response.completed", - response: { - status: "completed", - usage: { - input_tokens: 5, - output_tokens: 3, - total_tokens: 8, - input_tokens_details: { cached_tokens: 0 }, - }, + it.each([ + ["gpt-5.4", "minimal", "low"], + ["gpt-5.5", "minimal", "low"], + ["gpt-5.6-sol", "minimal", "low"], + ["gpt-5.6-terra", "minimal", "low"], + ["gpt-5.6-luna", "minimal", "low"], + ["gpt-5.6-sol", "xhigh", "xhigh"], + ["gpt-5.6-sol", "max", "max"], + ] as const)("maps %s reasoning effort %s to %s", async (modelId, effort, expectedEffort) => { + const tempDir = mkdtempSync(join(tmpdir(), "dreb-codex-stream-")); + process.env.DREB_CODING_AGENT_DIR = tempDir; + + const payload = Buffer.from( + JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }), + "utf8", + ).toString("base64"); + const token = `aaa.${payload}.bbb`; + + const sse = `${[ + `data: ${JSON.stringify({ + type: "response.output_item.added", + item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] }, + })}`, + `data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`, + `data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Hello" })}`, + `data: ${JSON.stringify({ + type: "response.output_item.done", + item: { + type: "message", + id: "msg_1", + role: "assistant", + status: "completed", + content: [{ type: "output_text", text: "Hello" }], + }, + })}`, + `data: ${JSON.stringify({ + type: "response.completed", + response: { + status: "completed", + usage: { + input_tokens: 5, + output_tokens: 3, + total_tokens: 8, + input_tokens_details: { cached_tokens: 0 }, }, - })}`, - ].join("\n\n")}\n\n`; - - const encoder = new TextEncoder(); - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode(sse)); - controller.close(); }, - }); + })}`, + ].join("\n\n")}\n\n`; - const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => { - const url = typeof input === "string" ? input : input.toString(); - if (url === "https://api.github.com/repos/openai/codex/releases/latest") { - return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 }); - } - if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) { - return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } }); - } - if (url === "https://chatgpt.com/backend-api/codex/responses") { - const body = typeof init?.body === "string" ? (JSON.parse(init.body) as Record) : null; - expect(body?.reasoning).toEqual({ effort: "low", summary: "auto" }); + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(sse)); + controller.close(); + }, + }); - return new Response(stream, { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); - } - return new Response("not found", { status: 404 }); - }); + const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => { + const url = typeof input === "string" ? input : input.toString(); + if (url === "https://api.github.com/repos/openai/codex/releases/latest") { + return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 }); + } + if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) { + return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } }); + } + if (url === "https://chatgpt.com/backend-api/codex/responses") { + const body = typeof init?.body === "string" ? (JSON.parse(init.body) as Record) : null; + expect(body?.reasoning).toEqual({ effort: expectedEffort, summary: "auto" }); - global.fetch = fetchMock as typeof fetch; - - const model: Model<"openai-codex-responses"> = { - id: modelId, - name: modelId, - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 400000, - maxTokens: 128000, - }; - - const context: Context = { - systemPrompt: "You are a helpful assistant.", - messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], - }; - - const streamResult = streamOpenAICodexResponses(model, context, { - apiKey: token, - reasoningEffort: "minimal", - }); - await streamResult.result(); - }, - ); + return new Response(stream, { + status: 200, + headers: { "content-type": "text/event-stream" }, + }); + } + return new Response("not found", { status: 404 }); + }); + + global.fetch = fetchMock as typeof fetch; + + const model: Model<"openai-codex-responses"> = { + id: modelId, + name: modelId, + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }; + + const context: Context = { + systemPrompt: "You are a helpful assistant.", + messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }], + }; + + const streamResult = streamOpenAICodexResponses(model, context, { + apiKey: token, + reasoningEffort: effort, + }); + await streamResult.result(); + }); it("applies flex service tier pricing (0.5x multiplier)", async () => { const tempDir = mkdtempSync(join(tmpdir(), "dreb-codex-stream-")); diff --git a/packages/ai/test/openai-completions-qwen-chat-template.test.ts b/packages/ai/test/openai-completions-qwen-chat-template.test.ts index e347aac0..3f6d9cb6 100644 --- a/packages/ai/test/openai-completions-qwen-chat-template.test.ts +++ b/packages/ai/test/openai-completions-qwen-chat-template.test.ts @@ -59,7 +59,7 @@ function getParams(): { }; } -type ReasoningLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; +type ReasoningLevel = "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; async function stream(model: Model<"openai-completions">, reasoning?: ReasoningLevel) { await streamSimple( diff --git a/packages/ai/test/supports-max.test.ts b/packages/ai/test/supports-max.test.ts new file mode 100644 index 00000000..fec7f6df --- /dev/null +++ b/packages/ai/test/supports-max.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { getModel, supportsMax } from "../src/models.js"; +import { resolveReasoningEffort } from "../src/providers/simple-options.js"; + +const base = getModel("openai", "gpt-5.6-sol")!; + +describe("supportsMax", () => { + it.each(["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "openai/gpt-5.6-sol"])( + "recognizes GPT-5.6 model id %s", + (id) => { + expect(supportsMax({ ...base, id })).toBe(true); + }, + ); + + it.each(["gpt-5.5", "gpt-5-mini", "claude-opus-5", "k3", "qwen3.8-27b"])("rejects non-GPT-5.6 model id %s", (id) => { + expect(supportsMax({ ...base, id })).toBe(false); + }); + + it("falls max back through xhigh before high", () => { + expect(resolveReasoningEffort({ ...base, id: "gpt-5.6-sol" }, "max")).toBe("max"); + expect(resolveReasoningEffort({ ...base, id: "gpt-5.5" }, "max")).toBe("xhigh"); + expect(resolveReasoningEffort({ ...base, id: "gpt-5-mini" }, "max")).toBe("high"); + }); +}); diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index d25d84e1..3760845d 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -396,7 +396,7 @@ Good Explore requests ask it to locate every renderer of a named component, enum Set `backgroundAgents.maxConcurrentSubagents` in `/settings`, dashboard Settings, or `settings.json` to control the concurrency of newly started parent sessions. A value of `0` removes the `subagent` tool from new parents and adds explicit system-prompt guidance that the parent must perform normally delegated work itself. -**Agent type and override inheritance:** The top-level `agent` parameter is inherited by parallel tasks and chain steps that don't specify their own. Precedence: per-task `agent` > top-level `agent` > default (`"Explore"`). The `model` and optional `thinking` parameters follow the same per-task-over-top-level inheritance. Explicit thinking accepts `off`, `minimal`, `low`, `medium`, `high`, or `xhigh`; unsupported levels for the resolved model fail before spawn. Omit `thinking` to preserve the child's normal settings/default behavior. +**Agent type and override inheritance:** The top-level `agent` parameter is inherited by parallel tasks and chain steps that don't specify their own. Precedence: per-task `agent` > top-level `agent` > default (`"Explore"`). The `model` and optional `thinking` parameters follow the same per-task-over-top-level inheritance. Explicit thinking accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; unsupported levels for the resolved model fail before spawn. Omit `thinking` to preserve the child's normal settings/default behavior. **Agent definitions** live in `~/.dreb/agents/` (global) and `.dreb/agents/` (project). Each is a markdown file with YAML frontmatter specifying `name`, `model` (with provider fallback list), and optional `systemPrompt`. Built-in agents include `Explore` (concrete evidence retrieval with no implementation work), `Sandbox` (restricted to `/tmp`), `feature-dev` (strong-tier coding), and several review agents. @@ -635,10 +635,12 @@ cat README.md | dreb -p "Summarize this text" | `--provider ` | Provider (anthropic, openai, google, etc.) | | `--model ` | Model pattern or ID (supports `provider/id` and optional `:`) | | `--api-key ` | API key (overrides env vars) | -| `--thinking ` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | +| `--thinking ` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` | | `--models ` | Comma-separated patterns for model cycling | | `--list-models [search]` | List available models | +`max` is a separate native effort currently supported by GPT-5.6 (including Sol, Terra, and Luna); `xhigh` remains available independently. Codex `ultra` is not a provider effort: it combines `max` with client-side multi-agent orchestration, so dreb does not send `ultra` as a raw value. + ### Session Options | Option | Description | diff --git a/packages/coding-agent/docs/agent-models.md b/packages/coding-agent/docs/agent-models.md index d017a7e2..9ad48cb0 100644 --- a/packages/coding-agent/docs/agent-models.md +++ b/packages/coding-agent/docs/agent-models.md @@ -40,7 +40,7 @@ If the `agentModels.models` list is empty or undefined for a given agent, resolu The `subagent` tool accepts an optional `thinking` value in single mode, at the top level for parallel/chain inheritance, or on an individual task/step. Per-task values win over the top-level value. -Supported values are `off`, `minimal`, `low`, `medium`, `high`, and `xhigh`. Validation happens after the child model resolves: non-`off` levels fail for non-reasoning models, and `xhigh` fails for models that do not advertise `xhigh` support. Omit the field to preserve the child's normal default/settings behavior. The child's actual effective level is reported in its `agent_start` event, result metadata, and `background_agent_end` event. +Supported values are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. Validation happens after the child model resolves: non-`off` levels fail for non-reasoning models, `xhigh` requires advertised xhigh support, and `max` requires a max-capable model (currently GPT-5.6 including Sol, Terra, and Luna). `max` does not replace `xhigh`. Codex `ultra` additionally enables client-side multi-agent orchestration, so it is not accepted or sent as a raw effort. Omit the field to preserve the child's normal default/settings behavior. The child's actual effective level is reported in its `agent_start` event, result metadata, and `background_agent_end` event. ## Evidence-based Routing Guide diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index 2f0f5ed9..92c0439a 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -198,7 +198,7 @@ models: [{ thinkingFormat: "qwen-chat-template" // The Qwen3.8+ default map below is applied automatically; supply your own // reasoningEffortMap to override it: - // reasoningEffortMap: { minimal: "low", low: "low", medium: "medium", high: "xhigh", xhigh: "xhigh" } + // reasoningEffortMap: { minimal: "low", low: "low", medium: "medium", high: "xhigh", xhigh: "xhigh", max: "xhigh" } } }] ``` @@ -631,7 +631,7 @@ interface ProviderModelConfig { supportsStore?: boolean; supportsDeveloperRole?: boolean; supportsReasoningEffort?: boolean; - reasoningEffortMap?: Partial>; + reasoningEffortMap?: Partial>; supportsUsageInStreaming?: boolean; maxTokensField?: "max_completion_tokens" | "max_tokens"; requiresToolResultName?: boolean; diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 9e6cc159..8170114e 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -1295,7 +1295,7 @@ if (model) { Get or set the thinking level. Level is clamped to model capabilities (non-reasoning models always use "off"). ```typescript -const current = dreb.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" +const current = dreb.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" dreb.setThinkingLevel("high"); ``` diff --git a/packages/coding-agent/docs/models.md b/packages/coding-agent/docs/models.md index 67f88680..213c8c03 100644 --- a/packages/coding-agent/docs/models.md +++ b/packages/coding-agent/docs/models.md @@ -344,7 +344,9 @@ For providers with partial OpenAI compatibility, use the `compat` field. | `openRouterRouting` | OpenRouter routing config passed to OpenRouter for model/provider selection | | `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) | -`qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking`. When reasoning is enabled, dreb sends the mapped effort as a top-level `reasoning_effort`; Qwen3.8+ models default-map dreb's levels onto the three native tiers (`minimal`/`low` → `low`, `medium` → `medium`, `high`/`xhigh` → `xhigh`). +The normalized scale is `minimal`, `low`, `medium`, `high`, `xhigh`, and `max` (`off` disables reasoning at the session layer). Native normalized `max` is model-aware and currently supported by GPT-5.6 aliases and Sol/Terra/Luna variants; unsupported defaults fall back `max` → `xhigh` → `high`. Existing provider mappings remain independent—for example, Claude can still map dreb's `xhigh` to Anthropic's provider-native `max`. Codex `ultra` also enables local multi-agent orchestration and is therefore not a raw effort value. + +`qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking`. When reasoning is enabled, dreb sends the mapped effort as a top-level `reasoning_effort`; Qwen3.8+ models default-map dreb's levels onto the three native tiers (`minimal`/`low` → `low`, `medium` → `medium`, `high`/`xhigh`/`max` → `xhigh`). Example: diff --git a/packages/coding-agent/docs/rpc.md b/packages/coding-agent/docs/rpc.md index 55fb434d..a9a7b38c 100644 --- a/packages/coding-agent/docs/rpc.md +++ b/packages/coding-agent/docs/rpc.md @@ -225,6 +225,7 @@ Response: {"id": "inspect", "title": "Inspect the implementation", "status": "in_progress"} ], "thinkingLevel": "medium", + "availableThinkingLevels": ["off", "minimal", "low", "medium", "high", "xhigh"], "isStreaming": false, "isRetrying": false, "retryAttempt": 0, @@ -486,9 +487,9 @@ Set the reasoning/thinking level for models that support it. {"type": "set_thinking_level", "level": "high"} ``` -Levels: `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` +Levels: `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"` -Note: `"xhigh"` is supported by GPT-5.2 through GPT-5.6 model families, Claude Opus 4.6–4.x and Claude 5 families (where it maps to adaptive effort `"max"`), and Kimi Code K3. +Note: `"xhigh"` is supported by GPT-5.2 through GPT-5.6 model families, Claude Opus 4.6–4.x and Claude 5 families (where it maps to adaptive effort `"max"`), and Kimi Code K3. The separate normalized `"max"` tier is model-aware and currently supported by GPT-5.6 aliases and Sol/Terra/Luna variants. Response: ```json @@ -1520,7 +1521,7 @@ Valid keys and values: | Key | Values | |-----|--------| | `defaultProvider` + `defaultModel` | Must be supplied together; must match a model from `get_available_models` | -| `defaultThinkingLevel` | `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` (validated against the full set — a stored default is not tied to the current model's capabilities) | +| `defaultThinkingLevel` | `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"` (validated against the full set — a stored default is not tied to the current model's capabilities) | | `steeringMode` | `"all"`, `"one-at-a-time"` | | `followUpMode` | `"all"`, `"one-at-a-time"` | | `compactionEnabled` | boolean | @@ -1541,7 +1542,7 @@ Errors are explicit `success: false` responses (nothing is applied on any of the - Missing/empty payload: `set_settings requires at least one setting to change` - Unknown key: `Unknown settings key(s): ...` -- Invalid enum value: `Invalid defaultThinkingLevel: "extreme". Valid values: off, minimal, low, medium, high, xhigh` +- Invalid enum value: `Invalid defaultThinkingLevel: "extreme". Valid values: off, minimal, low, medium, high, xhigh, max` - Invalid transport: `Invalid transport: "http". Valid values: sse, websocket, auto` - Non-boolean toggle: `Invalid retryEnabled: "yes". Must be a boolean` - Invalid `agentModels` object: `Invalid agentModels: must be a plain object mapping agent names to model fallback arrays` diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index 13218847..68fa3a65 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -306,7 +306,7 @@ const available = await modelRegistry.getAvailable(); const { session } = await createAgentSession({ model: opus, - thinkingLevel: "medium", // off, minimal, low, medium, high, xhigh + thinkingLevel: "medium", // off, minimal, low, medium, high, xhigh, max // Models for cycling (bindable via keybindings.json) scopedModels: [ diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 92f77e01..93b97a12 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -17,7 +17,7 @@ Edit directly or use `/settings` for common options. |---------|------|---------|-------------| | `defaultProvider` | string | - | Default provider (e.g., `"anthropic"`, `"openai"`) | | `defaultModel` | string | - | Default model ID | -| `defaultThinkingLevel` | string | - | `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` | +| `defaultThinkingLevel` | string | - | `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"` | | `hideThinkingBlock` | boolean | `false` | Hide thinking blocks in output | | `thinkingBudgets` | object | - | Custom token budgets per thinking level | | `agentModels.models` | object | - | Per-agent model fallback lists for subagents (map of agent name → ordered model IDs). See [agent-models.md](agent-models.md) | @@ -285,7 +285,7 @@ The optional Dispatch Arbiter is a fully headless, tool-less model call in the s |---------|------|---------|-------------| | `subagentArbiter.enabled` | boolean | `false` | Run fail-closed arbitration before every actual child spawn | | `subagentArbiter.model` | string | - | Required when enabled; exact canonical `provider/model` used for the direct arbiter call | -| `subagentArbiter.thinking` | string | `off` | Optional arbiter-call thinking: `off`, `minimal`, `low`, `medium`, `high`, or `xhigh`; explicit values are capability-validated | +| `subagentArbiter.thinking` | string | `off` | Optional arbiter-call thinking: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; explicit values are capability-validated | | `subagentArbiter.guidePath` | string | `~/.dreb/agent/model-routing-guide.md` | Routing guide generated by `/skill:model-routing-guide`; `~` expands normally and relative paths resolve against the child cwd | ```json diff --git a/packages/coding-agent/docs/themes.md b/packages/coding-agent/docs/themes.md index 36f2abd0..809a3b8a 100644 --- a/packages/coding-agent/docs/themes.md +++ b/packages/coding-agent/docs/themes.md @@ -109,6 +109,7 @@ vim ~/.dreb/agent/themes/my-theme.json "thinkingMedium": "#00ffff", "thinkingHigh": "#ff00ff", "thinkingXhigh": "#ff0000", + "thinkingMax": "#ff1493", "bashMode": "#ffaa00" } } @@ -216,7 +217,7 @@ Every theme must define all 51 color tokens. There are no optional colors. | `syntaxOperator` | Operators | | `syntaxPunctuation` | Punctuation | -### Thinking Level Borders (6 colors) +### Thinking Level Borders (7 colors) Editor border colors indicating thinking level (visual hierarchy from subtle to prominent): @@ -228,6 +229,7 @@ Editor border colors indicating thinking level (visual hierarchy from subtle to | `thinkingMedium` | Medium thinking | | `thinkingHigh` | High thinking | | `thinkingXhigh` | Extra high thinking | +| `thinkingMax` | Maximum thinking effort | ### Bash Mode (1 color) diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index b03dc5a9..d836a820 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -415,7 +415,7 @@ renderResult(result, options, theme, context) { | Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` | | Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` | | Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` | -| Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` | +| Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` | | Modes | `bashMode` | **Background colors** (`theme.bg(color, text)`): diff --git a/packages/coding-agent/examples/extensions/dynamic-resources/dynamic.json b/packages/coding-agent/examples/extensions/dynamic-resources/dynamic.json index c4c9af70..306f50d6 100644 --- a/packages/coding-agent/examples/extensions/dynamic-resources/dynamic.json +++ b/packages/coding-agent/examples/extensions/dynamic-resources/dynamic.json @@ -69,6 +69,7 @@ "thinkingMedium": "#81a2be", "thinkingHigh": "#b294bb", "thinkingXhigh": "#d183e8", + "thinkingMax": "#ff6b9d", "bashMode": "green" }, "export": { diff --git a/packages/coding-agent/examples/extensions/preset.ts b/packages/coding-agent/examples/extensions/preset.ts index eea34388..5a732ab8 100644 --- a/packages/coding-agent/examples/extensions/preset.ts +++ b/packages/coding-agent/examples/extensions/preset.ts @@ -51,7 +51,7 @@ interface Preset { /** Model ID (e.g., "claude-sonnet-4-5") */ model?: string; /** Thinking level */ - thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; + thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; /** Tools to enable (replaces default set) */ tools?: string[]; /** Instructions to append to system prompt */ diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index ede8e153..80ffe4d0 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@dreb/coding-agent", - "version": "2.61.0", + "version": "2.61.1", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "drebConfig": { diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 1650a058..ac03218a 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -51,7 +51,7 @@ export interface Args { unknownFlags: Map; } -const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const; +const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const; export function isValidThinkingLevel(level: string): level is ThinkingLevel { return VALID_THINKING_LEVELS.includes(level as ThinkingLevel); @@ -230,7 +230,7 @@ ${chalk.bold("Options:")} --tools Comma-separated list of standard tools to enable (default: all) Available: read, bash, edit, write, grep, find, ls, web_search, web_fetch, subagent, wait, watch_github_ci, ask_user Always active: search, skill, tasks_update - --thinking Set thinking level: off, minimal, low, medium, high, xhigh + --thinking Set thinking level: off, minimal, low, medium, high, xhigh, max --extension, -e Load an extension file (can be used multiple times) --no-extensions, -ne Disable extension discovery (explicit -e paths still work) --skill Load a skill file or directory (can be used multiple times) diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 2cf3750a..e3be95c9 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -18,7 +18,7 @@ import { tmpdir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@dreb/agent-core"; import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@dreb/ai"; -import { isContextOverflow, modelsAreEqual, resetApiProviders, supportsXhigh } from "@dreb/ai"; +import { isContextOverflow, modelsAreEqual, resetApiProviders, supportsMax, supportsXhigh } from "@dreb/ai"; import { getDocsPath } from "../config.js"; import { theme } from "../modes/interactive/theme/theme.js"; import { sleep } from "../utils/sleep.js"; @@ -297,8 +297,11 @@ interface ToolDefinitionEntry { /** Standard thinking levels */ const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"]; -/** Thinking levels including xhigh (for supported models) */ +/** Thinking levels including xhigh (for supported models). */ const THINKING_LEVELS_WITH_XHIGH: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; + +/** Complete ordered scale, including the native max tier for supported models. */ +const THINKING_LEVELS_WITH_MAX: ThinkingLevel[] = [...THINKING_LEVELS_WITH_XHIGH, "max"]; const MIN_PERFORMANCE_DURATION_MS = 10; // ============================================================================ @@ -2354,6 +2357,7 @@ export class AgentSession { */ getAvailableThinkingLevels(): ThinkingLevel[] { if (!this.supportsThinking()) return ["off"]; + if (this.supportsMaxThinking()) return THINKING_LEVELS_WITH_MAX; return this.supportsXhighThinking() ? THINKING_LEVELS_WITH_XHIGH : THINKING_LEVELS; } @@ -2364,6 +2368,11 @@ export class AgentSession { return this.model ? supportsXhigh(this.model) : false; } + /** Check if the current model supports the native max thinking level. */ + supportsMaxThinking(): boolean { + return this.model ? supportsMax(this.model) : false; + } + /** * Check if current model supports thinking/reasoning. */ @@ -2382,7 +2391,7 @@ export class AgentSession { } private _clampThinkingLevel(level: ThinkingLevel, availableLevels: ThinkingLevel[]): ThinkingLevel { - const ordered = THINKING_LEVELS_WITH_XHIGH; + const ordered = THINKING_LEVELS_WITH_MAX; const available = new Set(availableLevels); const requestedIndex = ordered.indexOf(level); if (requestedIndex === -1) { diff --git a/packages/coding-agent/src/core/dispatch-arbiter.ts b/packages/coding-agent/src/core/dispatch-arbiter.ts index 2d7c1cdd..4d4cf7f2 100644 --- a/packages/coding-agent/src/core/dispatch-arbiter.ts +++ b/packages/coding-agent/src/core/dispatch-arbiter.ts @@ -15,7 +15,7 @@ const MAX_METADATA_CHARS = 2_000; const MAX_CONTEXT_ENTRY_CHARS = 2_000; const MAX_ARBITER_PACKAGE_CHARS = 180_000; const MAX_SAFE_ERROR_CHARS = 500; -const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]); +const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]); export interface DispatchRoute { agent: string; @@ -373,7 +373,7 @@ export class DispatchArbiter { context.messages.push({ role: "user", content: - 'Your previous response did not match the required exact three-key JSON object. Return only {"agent":string,"model":"provider/model","thinking":"off|minimal|low|medium|high|xhigh"}.', + 'Your previous response did not match the required exact three-key JSON object. Return only {"agent":string,"model":"provider/model","thinking":"off|minimal|low|medium|high|xhigh|max"}.', timestamp: Date.now(), }); } diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index a10842dc..691e49b5 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -49,6 +49,7 @@ const ReasoningEffortMapSchema = Type.Object({ medium: Type.Optional(Type.String()), high: Type.Optional(Type.String()), xhigh: Type.Optional(Type.String()), + max: Type.Optional(Type.String()), }); const OpenAICompletionsCompatSchema = Type.Object({ diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 7eb26330..7818b00a 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -82,7 +82,7 @@ export interface SubagentArbiterSettings { enabled?: boolean; /** Exact canonical provider/model used for the tool-less arbiter call. */ model?: string; - thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; + thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; guidePath?: string; } @@ -117,7 +117,7 @@ export type PackageSource = export interface Settings { defaultProvider?: string; defaultModel?: string; - defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; + defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; transport?: TransportSetting; // default: "sse" steeringMode?: "all" | "one-at-a-time"; followUpMode?: "all" | "one-at-a-time"; @@ -743,11 +743,11 @@ export class SettingsManager { this.save(); } - getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined { + getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | undefined { return this.settings.defaultThinkingLevel; } - setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): void { + setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"): void { this.globalSettings.defaultThinkingLevel = level; this.markModified("defaultThinkingLevel"); this.save(); diff --git a/packages/coding-agent/src/core/thinking.ts b/packages/coding-agent/src/core/thinking.ts index aabee6a0..e2ed5008 100644 --- a/packages/coding-agent/src/core/thinking.ts +++ b/packages/coding-agent/src/core/thinking.ts @@ -1,5 +1,11 @@ import type { ThinkingLevel as AgentThinkingLevel } from "@dreb/agent-core"; -import { type ThinkingLevel as AiThinkingLevel, type Model, supportsAdaptiveThinking, supportsXhigh } from "@dreb/ai"; +import { + type ThinkingLevel as AiThinkingLevel, + type Model, + supportsAdaptiveThinking, + supportsMax, + supportsXhigh, +} from "@dreb/ai"; import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; /** @@ -13,6 +19,7 @@ export function resolveEffectiveThinkingLevel( ): AgentThinkingLevel { const effectiveThinkingLevel = thinkingLevel ?? defaultThinkingLevel; if (!model?.reasoning) return "off"; + if (effectiveThinkingLevel === "max" && !supportsMax(model)) return supportsXhigh(model) ? "xhigh" : "high"; return effectiveThinkingLevel === "xhigh" && !supportsXhigh(model) ? "high" : effectiveThinkingLevel; } @@ -48,6 +55,12 @@ export function validateThinkingLevelForModel( error: `Thinking level "${thinkingLevel}" is not supported by non-reasoning model "${modelRef}". Use "off" or choose a reasoning model.`, }; } + if (thinkingLevel === "max" && !supportsMax(model)) { + return { + ok: false, + error: `Thinking level "max" is not supported by model "${modelRef}". Use "xhigh" or choose a max-capable model.`, + }; + } if (thinkingLevel === "xhigh" && !supportsXhigh(model)) { return { ok: false, diff --git a/packages/coding-agent/src/core/tools/subagent.ts b/packages/coding-agent/src/core/tools/subagent.ts index b3ac92cb..bf6f0ac4 100644 --- a/packages/coding-agent/src/core/tools/subagent.ts +++ b/packages/coding-agent/src/core/tools/subagent.ts @@ -43,7 +43,7 @@ export interface AgentTypeConfig { } const DEFAULT_AGENT = "Explore"; -const SUBAGENT_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const; +const SUBAGENT_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const; export const DEFAULT_MODEL_AVAILABILITY_PROBE_TIMEOUT_MS = 120_000; export function parseAgentFrontmatter( @@ -1970,6 +1970,7 @@ const thinkingLevelSchema = Type.Union( Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh"), + Type.Literal("max"), ], { description: "Thinking level override for the child model." }, ); @@ -2010,6 +2011,7 @@ const subagentSchema = Type.Object({ Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh"), + Type.Literal("max"), ], { description: @@ -2214,7 +2216,7 @@ export function createSubagentToolDefinition( "Each agent notifies independently when done — completion messages include a list of any still-running agents. If you need their results before proceeding, end your current turn with no tool calls (as if you were asking the user a question and waiting for their reply). This emits `agent_end` and lets the framework deliver the completion as a new message that resumes your turn automatically. Do not call `sleep` or any other waiting action, and do not launch filler work.", "Agent definitions specify a `model` field with a provider fallback list (comma-separated or YAML list). The spawner tries each in order and uses the first one that resolves for the current provider. This makes agents portable across providers.", "Per-invocation `model` overrides take precedence but **discard the entire fallback list** — if the single override model isn't available on the current provider, the agent fails. Only override when you have a specific reason (e.g. escalating to a stronger tier for a complex task).", - "Optional `thinking` overrides accept off/minimal/low/medium/high/xhigh. Per-task values override a top-level value; unsupported levels fail before spawn. Omit thinking to preserve the child's configured default.", + "Optional `thinking` overrides accept off/minimal/low/medium/high/xhigh/max. Per-task values override a top-level value; unsupported levels fail before spawn. Omit thinking to preserve the child's configured default.", "**Model routing** — choose an agent by role fit first. Agent definitions already specify the normal model tier for that role; override the model only when the assigned task genuinely requires a different capability tier.", "**Model identity** — Your current model is stated in the system prompt as `You are running on: provider/id`. Use this for explicit routing decisions — e.g. delegate vision tasks if you're on a text-only model, or use a differently-architected model as a critic for tasks where diverse model perspectives improve reliability.", ], diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 5a227bcf..5c1e9f2d 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -5,7 +5,7 @@ * createAgentSession() options. The SDK does the heavy lifting. */ -import { type ImageContent, modelsAreEqual, supportsXhigh } from "@dreb/ai"; +import { type ImageContent, modelsAreEqual, supportsMax, supportsXhigh } from "@dreb/ai"; import chalk from "chalk"; import { createInterface } from "readline"; import { type Args, parseArgs, printHelp } from "./cli/args.js"; @@ -934,6 +934,15 @@ export async function main(args: string[]) { // This covers both --thinking and --model :. const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel; if (session.model && cliThinkingOverride) { + const requestedThinking = parsed.thinking ?? sessionOptions.thinkingLevel; + if (requestedThinking === "max" && !supportsMax(session.model)) { + log.error( + chalk.red( + `Thinking level "max" is not supported by model "${session.model.provider}/${session.model.id}". Use "xhigh" or choose a max-capable model.`, + ), + ); + process.exit(1); + } let effectiveThinking = session.thinkingLevel; if (!session.model.reasoning) { effectiveThinking = "off"; diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index 02db29ae..2f6234c8 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -33,7 +33,8 @@ const THINKING_DESCRIPTIONS: Record = { low: "Light reasoning (~2k tokens)", medium: "Moderate reasoning (~8k tokens)", high: "Deep reasoning (~16k tokens)", - xhigh: "Maximum reasoning (~32k tokens)", + xhigh: "Extra-high reasoning (~32k tokens)", + max: "Maximum reasoning effort", }; export interface SettingsConfig { @@ -658,7 +659,7 @@ export class SettingsSelectorComponent extends Container { new SelectSubmenu( "Dispatch Arbiter Thinking", "Select reasoning depth for the headless arbiter call", - (["off", "minimal", "low", "medium", "high", "xhigh"] as ThinkingLevel[]).map((level) => ({ + (["off", "minimal", "low", "medium", "high", "xhigh", "max"] as ThinkingLevel[]).map((level) => ({ value: level, label: level, description: THINKING_DESCRIPTIONS[level], diff --git a/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts b/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts index ba88fd41..4ae68160 100644 --- a/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts @@ -14,7 +14,8 @@ const LEVEL_DESCRIPTIONS: Record = { low: "Light reasoning (~2k tokens)", medium: "Moderate reasoning (~8k tokens)", high: "Deep reasoning (~16k tokens)", - xhigh: "Maximum reasoning (~32k tokens)", + xhigh: "Extra-high reasoning (~32k tokens)", + max: "Maximum reasoning effort", }; /** diff --git a/packages/coding-agent/src/modes/interactive/theme/dark.json b/packages/coding-agent/src/modes/interactive/theme/dark.json index bdac7694..132d105a 100644 --- a/packages/coding-agent/src/modes/interactive/theme/dark.json +++ b/packages/coding-agent/src/modes/interactive/theme/dark.json @@ -30,7 +30,6 @@ "dim": "dimGray", "text": "", "thinkingText": "gray", - "selectedBg": "selectedBg", "userMessageBg": "userMsgBg", "userMessageText": "", @@ -42,7 +41,6 @@ "toolErrorBg": "toolErrorBg", "toolTitle": "", "toolOutput": "gray", - "mdHeading": "#f0c674", "mdLink": "#81a2be", "mdLinkUrl": "dimGray", @@ -53,11 +51,9 @@ "mdQuoteBorder": "gray", "mdHr": "gray", "mdListBullet": "accent", - "toolDiffAdded": "green", "toolDiffRemoved": "red", "toolDiffContext": "gray", - "syntaxComment": "#6A9955", "syntaxKeyword": "#569CD6", "syntaxFunction": "#DCDCAA", @@ -67,14 +63,13 @@ "syntaxType": "#4EC9B0", "syntaxOperator": "#D4D4D4", "syntaxPunctuation": "#D4D4D4", - "thinkingOff": "darkGray", "thinkingMinimal": "#6e6e6e", "thinkingLow": "#5f87af", "thinkingMedium": "#81a2be", "thinkingHigh": "#b294bb", "thinkingXhigh": "#d183e8", - + "thinkingMax": "#ff6b9d", "bashMode": "green" }, "export": { diff --git a/packages/coding-agent/src/modes/interactive/theme/light.json b/packages/coding-agent/src/modes/interactive/theme/light.json index 0339acc2..d99721af 100644 --- a/packages/coding-agent/src/modes/interactive/theme/light.json +++ b/packages/coding-agent/src/modes/interactive/theme/light.json @@ -29,7 +29,6 @@ "dim": "dimGray", "text": "", "thinkingText": "mediumGray", - "selectedBg": "selectedBg", "userMessageBg": "userMsgBg", "userMessageText": "", @@ -41,7 +40,6 @@ "toolErrorBg": "toolErrorBg", "toolTitle": "", "toolOutput": "mediumGray", - "mdHeading": "yellow", "mdLink": "blue", "mdLinkUrl": "dimGray", @@ -52,11 +50,9 @@ "mdQuoteBorder": "mediumGray", "mdHr": "mediumGray", "mdListBullet": "green", - "toolDiffAdded": "green", "toolDiffRemoved": "red", "toolDiffContext": "mediumGray", - "syntaxComment": "#008000", "syntaxKeyword": "#0000FF", "syntaxFunction": "#795E26", @@ -66,14 +62,13 @@ "syntaxType": "#267F99", "syntaxOperator": "#000000", "syntaxPunctuation": "#000000", - "thinkingOff": "lightGray", "thinkingMinimal": "#767676", "thinkingLow": "blue", "thinkingMedium": "teal", "thinkingHigh": "#875f87", "thinkingXhigh": "#8b008b", - + "thinkingMax": "#b00040", "bashMode": "green" }, "export": { diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json index 7bc495da..def0123d 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json +++ b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json @@ -3,7 +3,10 @@ "title": "Pi Coding Agent Theme", "description": "Theme schema for Pi coding agent", "type": "object", - "required": ["name", "colors"], + "required": [ + "name", + "colors" + ], "properties": { "$schema": { "type": "string", @@ -85,6 +88,7 @@ "thinkingMedium", "thinkingHigh", "thinkingXhigh", + "thinkingMax", "bashMode" ], "properties": { @@ -286,7 +290,11 @@ }, "thinkingXhigh": { "$ref": "#/$defs/colorValue", - "description": "Thinking level border: xhigh (OpenAI codex-max only)" + "description": "Thinking level border: xhigh" + }, + "thinkingMax": { + "$ref": "#/$defs/colorValue", + "description": "Thinking level border: max" }, "bashMode": { "$ref": "#/$defs/colorValue", diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index c30ce133..2e0ec035 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -73,13 +73,14 @@ const ThemeJsonSchema = Type.Object({ syntaxType: ColorValueSchema, syntaxOperator: ColorValueSchema, syntaxPunctuation: ColorValueSchema, - // Thinking Level Borders (6 colors) + // Thinking Level Borders (7 colors) thinkingOff: ColorValueSchema, thinkingMinimal: ColorValueSchema, thinkingLow: ColorValueSchema, thinkingMedium: ColorValueSchema, thinkingHigh: ColorValueSchema, thinkingXhigh: ColorValueSchema, + thinkingMax: ColorValueSchema, // Bash Mode (1 color) bashMode: ColorValueSchema, }), @@ -141,6 +142,7 @@ export type ThemeColor = | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" + | "thinkingMax" | "bashMode"; export type ThemeBg = @@ -415,7 +417,9 @@ export class Theme { return this.mode; } - getThinkingBorderColor(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): (str: string) => string { + getThinkingBorderColor( + level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", + ): (str: string) => string { // Map thinking levels to dedicated theme colors switch (level) { case "off": @@ -430,6 +434,8 @@ export class Theme { return (str: string) => this.fg("thinkingHigh", str); case "xhigh": return (str: string) => this.fg("thinkingXhigh", str); + case "max": + return (str: string) => this.fg("thinkingMax", str); default: return (str: string) => this.fg("thinkingOff", str); } @@ -690,8 +696,11 @@ export function initTheme(themeName?: string, enableWatcher: boolean = false): v if (enableWatcher) { startThemeWatcher(); } - } catch (_error) { - // Theme is invalid - fall back to dark theme silently + } catch (error) { + // Theme is invalid - warn the user and fall back to dark theme + if (error instanceof Error) { + process.stderr.write(`\n${error.message}\nFalling back to dark theme.\n\n`); + } currentThemeName = "dark"; setGlobalTheme(loadTheme("dark")); // Don't start watcher for fallback theme diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 009c885a..cee4ff2b 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -287,6 +287,7 @@ export function getStateForRpc(session: AgentSession, modelFallbackMessage?: str tasks: session.tasks.map((task) => ({ ...task })), usingSubscription: session.model ? session.modelRegistry.isUsingOAuth(session.model) : false, thinkingLevel: session.thinkingLevel, + availableThinkingLevels: session.getAvailableThinkingLevels(), isStreaming: session.isStreaming, isRetrying: session.isRetrying, retryAttempt: session.retryAttempt, diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index 73266648..ca4aa777 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -238,6 +238,8 @@ export interface RpcSessionState { tasks: RpcSessionTask[]; usingSubscription: boolean; thinkingLevel: ThinkingLevel; + /** Model-aware levels available for selection and cycling. */ + availableThinkingLevels: ThinkingLevel[]; isStreaming: boolean; /** True from retry classification through retry completion, including backoff. */ isRetrying: boolean; diff --git a/packages/coding-agent/test/agent-session-model-switch-thinking.test.ts b/packages/coding-agent/test/agent-session-model-switch-thinking.test.ts index bc1cdf44..eabcceb6 100644 --- a/packages/coding-agent/test/agent-session-model-switch-thinking.test.ts +++ b/packages/coding-agent/test/agent-session-model-switch-thinking.test.ts @@ -13,6 +13,8 @@ import { createTestResourceLoader } from "./utilities.js"; const reasoningModel = findModel("anthropic", "sonnet")!; const nonReasoningModel = findModel("openai", "gpt-4o-mini")!; +const xhighModel = findModel("openai", "gpt-5.5")!; +const maxModel = findModel("openai", "gpt-5.6-sol")!; // Adaptive-thinking model (Opus/Sonnet 4.6+): thinkingDisplay is honored, defaults to "summarized". const adaptiveModel = findModel("anthropic", "opus-4-8")!; @@ -101,6 +103,34 @@ function createThinkingDisplaySession(settingsManager: SettingsManager = Setting } describe("AgentSession model switching", () => { + it("exposes max only for max-capable models and clamps max to xhigh on model switch", async () => { + const { session } = createSession({ + thinkingLevel: "max", + scopedModels: [{ model: maxModel }, { model: xhighModel }], + }); + + try { + await session.setModel(maxModel); + expect(session.getAvailableThinkingLevels()).toEqual([ + "off", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", + ]); + session.setThinkingLevel("max"); + expect(session.thinkingLevel).toBe("max"); + + await session.setModel(xhighModel); + expect(session.getAvailableThinkingLevels()).not.toContain("max"); + expect(session.thinkingLevel).toBe("xhigh"); + } finally { + session.dispose(); + } + }); + it("preserves the saved thinking preference through non-reasoning models", async () => { const { session, sessionManager, settingsManager } = createSession({ scopedModels: [{ model: reasoningModel }, { model: nonReasoningModel }], diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index b155fe8b..f0c8c4e1 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -107,7 +107,7 @@ describe("parseModelPattern", () => { }); test("all valid thinking levels work", () => { - for (const level of ["off", "minimal", "low", "medium", "high", "xhigh"]) { + for (const level of ["off", "minimal", "low", "medium", "high", "xhigh", "max"]) { const result = parseModelPattern(`sonnet:${level}`, allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); expect(result.thinkingLevel).toBe(level); diff --git a/packages/coding-agent/test/rpc-dashboard-commands.test.ts b/packages/coding-agent/test/rpc-dashboard-commands.test.ts index 98ab63f9..a2e63c1f 100644 --- a/packages/coding-agent/test/rpc-dashboard-commands.test.ts +++ b/packages/coding-agent/test/rpc-dashboard-commands.test.ts @@ -638,6 +638,7 @@ describe("RpcClient dashboard command methods", () => { tasks: [], usingSubscription: false, thinkingLevel: "high", + availableThinkingLevels: ["off", "minimal", "low", "medium", "high"], isStreaming: false, isRetrying: false, retryAttempt: 0, diff --git a/packages/coding-agent/test/subagent-thinking-override.test.ts b/packages/coding-agent/test/subagent-thinking-override.test.ts index deea2460..ad3d5b23 100644 --- a/packages/coding-agent/test/subagent-thinking-override.test.ts +++ b/packages/coding-agent/test/subagent-thinking-override.test.ts @@ -171,7 +171,7 @@ afterEach(() => { }); describe("subagent thinking schema", () => { - test.each(["off", "minimal", "low", "medium", "high", "xhigh"] satisfies ThinkingLevel[])( + test.each(["off", "minimal", "low", "medium", "high", "xhigh", "max"] satisfies ThinkingLevel[])( "accepts %s in single, parallel, and chain modes", (thinking) => { expect(Value.Check(subagentToolDefinition.parameters, { task: "single", thinking })).toBe(true); diff --git a/packages/coding-agent/test/thinking.test.ts b/packages/coding-agent/test/thinking.test.ts index 45ebdd92..0a47e6e0 100644 --- a/packages/coding-agent/test/thinking.test.ts +++ b/packages/coding-agent/test/thinking.test.ts @@ -53,9 +53,18 @@ describe("resolveEffectiveThinkingLevel", () => { }); test("xhigh-capable models preserve xhigh for normal session defaults", () => { - const xhighModel = { ...reasoningModel, id: "gpt-5.6-test" } as Model; + const xhighModel = { ...reasoningModel, id: "gpt-5.5" } as Model; expect(resolveEffectiveThinkingLevel(xhighModel, "xhigh")).toBe("xhigh"); }); + + test("unsupported max falls back to xhigh before high", () => { + expect(resolveEffectiveThinkingLevel({ ...reasoningModel, id: "gpt-5.5" }, "max")).toBe("xhigh"); + expect(resolveEffectiveThinkingLevel(reasoningModel, "max")).toBe("high"); + }); + + test("max-capable models preserve max", () => { + expect(resolveEffectiveThinkingLevel({ ...reasoningModel, id: "gpt-5.6-sol" }, "max")).toBe("max"); + }); }); describe("thinkingLevelToReasoning", () => { @@ -63,7 +72,7 @@ describe("thinkingLevelToReasoning", () => { expect(thinkingLevelToReasoning("off")).toBeUndefined(); }); - test.each(["minimal", "low", "medium", "high", "xhigh"] satisfies AiThinkingLevel[])( + test.each(["minimal", "low", "medium", "high", "xhigh", "max"] satisfies AiThinkingLevel[])( "passes through %s", (thinkingLevel) => { expect(thinkingLevelToReasoning(thinkingLevel)).toBe(thinkingLevel); @@ -72,7 +81,8 @@ describe("thinkingLevelToReasoning", () => { }); describe("validateThinkingLevelForModel", () => { - const xhighModel = { ...reasoningModel, id: "gpt-5.6-test" } as Model; + const xhighModel = { ...reasoningModel, id: "gpt-5.5" } as Model; + const maxModel = { ...reasoningModel, id: "gpt-5.6-sol" } as Model; test("accepts off without a resolved model", () => { expect(validateThinkingLevelForModel(undefined, "off")).toEqual({ ok: true }); @@ -90,6 +100,11 @@ describe("validateThinkingLevelForModel", () => { expect(validateThinkingLevelForModel(reasoningModel, "xhigh")).toMatchObject({ ok: false }); }); + test("accepts max only for max-capable models", () => { + expect(validateThinkingLevelForModel(maxModel, "max")).toEqual({ ok: true }); + expect(validateThinkingLevelForModel(xhighModel, "max")).toMatchObject({ ok: false }); + }); + test("rejects non-off thinking for non-reasoning models", () => { const result = validateThinkingLevelForModel(nonReasoningModel, "high"); expect(result).toMatchObject({ ok: false }); diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 664516cf..2a203188 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@dreb/dashboard", - "version": "2.61.0", + "version": "2.61.1", "description": "Web dashboard for dreb — fleet overview, chat parity, subagent observability", "license": "MIT", "type": "module", diff --git a/packages/dashboard/src/client/api.ts b/packages/dashboard/src/client/api.ts index ab7de710..35400ff3 100644 --- a/packages/dashboard/src/client/api.ts +++ b/packages/dashboard/src/client/api.ts @@ -138,10 +138,12 @@ export const api = { abortCompaction: (key: string) => request<{ ok: true }>(`/api/runtimes/${key}/abort-compaction`, { method: "POST" }), abortRetry: (key: string) => request<{ ok: true }>(`/api/runtimes/${key}/abort-retry`, { method: "POST" }), setModel: (key: string, provider: string, modelId: string) => - request<{ provider: string; id: string; settingsRevision: number }>( - `/api/runtimes/${key}/model`, - json({ provider, modelId }), - ), + request<{ + model: { provider: string; id: string }; + thinkingLevel: string; + availableThinkingLevels: string[]; + settingsRevision: number; + }>(`/api/runtimes/${key}/model`, json({ provider, modelId })), models: (key: string) => request<{ models: ModelInfoDto[] }>(`/api/runtimes/${key}/models`), setThinking: (key: string, level: string) => request<{ ok: true; settingsRevision: number }>(`/api/runtimes/${key}/thinking`, json({ level })), diff --git a/packages/dashboard/src/client/screens/session.tsx b/packages/dashboard/src/client/screens/session.tsx index bb413541..d91b36ac 100644 --- a/packages/dashboard/src/client/screens/session.tsx +++ b/packages/dashboard/src/client/screens/session.tsx @@ -37,7 +37,6 @@ import { import type { AskUiQuestion, ExtensionUiRequest, SessionViewState } from "../state/reducer.js"; import type { AppStore } from "../state/store.js"; -const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"]; const MAX_IMAGE_BYTES = 10 * 1024 * 1024; const UPLOAD_DIR_NAME = ".dreb-dashboard-uploads"; @@ -614,7 +613,12 @@ function ModelSelectorModal(props: { state?: SessionStateDto; initialFilter?: string; onClose: () => void; - onSelected: (model: { provider: string; id: string; settingsRevision: number }) => void; + onSelected: (result: { + model: { provider: string; id: string }; + thinkingLevel: string; + availableThinkingLevels: string[]; + settingsRevision: number; + }) => void; }): JSX.Element { const [models, setModels] = createSignal([]); const [filter, setFilter] = createSignal(props.initialFilter ?? ""); @@ -864,6 +868,7 @@ function ImportModal(props: { export function SessionScreen(props: { store: AppStore; sessionKey: string }): JSX.Element { const session = (): SessionViewState | undefined => props.store.sessions[props.sessionKey]; const runtime = createMemo(() => props.store.fleet().runtimes.find((r) => r.key === props.sessionKey)); + const availableThinkingLevels = createMemo(() => runtime()?.state.availableThinkingLevels ?? ["off"]); const [composerText, setComposerText] = createSignal(getComposerDraft(props.sessionKey) ?? ""); const [sendMode, setSendMode] = createSignal<"steer" | "follow_up">("steer"); @@ -1847,8 +1852,8 @@ export function SessionScreen(props: { store: AppStore; sessionKey: string }): J class="switcher optional" onClick={async () => { const current = runtime()?.state.thinkingLevel ?? "off"; - const next = - THINKING_LEVELS[(THINKING_LEVELS.indexOf(current) + 1) % THINKING_LEVELS.length]; + const levels = availableThinkingLevels(); + const next = levels[(levels.indexOf(current) + 1) % levels.length]; try { const result = await api.setThinking(props.sessionKey, next); props.store.setRuntimeThinkingLevel(props.sessionKey, next, result.settingsRevision); @@ -1956,8 +1961,8 @@ export function SessionScreen(props: { store: AppStore; sessionKey: string }): J class="btn btn-small" onClick={async () => { const current = runtime()?.state.thinkingLevel ?? "off"; - const next = - THINKING_LEVELS[(THINKING_LEVELS.indexOf(current) + 1) % THINKING_LEVELS.length]; + const levels = availableThinkingLevels(); + const next = levels[(levels.indexOf(current) + 1) % levels.length]; try { const result = await api.setThinking(props.sessionKey, next); props.store.setRuntimeThinkingLevel(props.sessionKey, next, result.settingsRevision); @@ -2422,9 +2427,7 @@ export function SessionScreen(props: { store: AppStore; sessionKey: string }): J setShowModelSelector(false); setModelFilter(""); }} - onSelected={({ provider, id, settingsRevision }) => - props.store.setRuntimeModel(props.sessionKey, { provider, id }, settingsRevision) - } + onSelected={(result) => props.store.setRuntimeModel(props.sessionKey, result)} /> diff --git a/packages/dashboard/src/client/screens/settings.tsx b/packages/dashboard/src/client/screens/settings.tsx index 2eb71691..acc732f7 100644 --- a/packages/dashboard/src/client/screens/settings.tsx +++ b/packages/dashboard/src/client/screens/settings.tsx @@ -37,7 +37,7 @@ import { } from "../state/preferences.js"; import type { AppStore } from "../state/store.js"; -const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"]; +const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"]; const QUEUE_MODES = ["all", "one-at-a-time"] as const; const TRANSPORTS = ["sse", "websocket", "auto"] as const; @@ -655,7 +655,8 @@ export function SettingsScreen(props: { | "low" | "medium" | "high" - | "xhigh", + | "xhigh" + | "max", }) } > diff --git a/packages/dashboard/src/client/state/store.ts b/packages/dashboard/src/client/state/store.ts index 5e96cf72..0553c4d8 100644 --- a/packages/dashboard/src/client/state/store.ts +++ b/packages/dashboard/src/client/state/store.ts @@ -132,6 +132,7 @@ interface PendingRuntimeSetting { interface PendingRuntimeSettings { model?: PendingRuntimeSetting>; thinkingLevel?: PendingRuntimeSetting; + availableThinkingLevels?: PendingRuntimeSetting; } function restoreSnapshotOutcomeState(session: SessionViewState, messages: any[], snapshotState: SessionStateDto): void { @@ -466,8 +467,21 @@ export function createAppStore() { if (settingsRevision >= pending.thinkingLevel.settingsRevision) delete pending.thinkingLevel; else state.thinkingLevel = previous?.state.thinkingLevel ?? pending.thinkingLevel.value; } - if (pending.model === undefined && pending.thinkingLevel === undefined) + if (pending.availableThinkingLevels) { + if (settingsRevision >= pending.availableThinkingLevels.settingsRevision) + delete pending.availableThinkingLevels; + else { + state.availableThinkingLevels = + previous?.state.availableThinkingLevels ?? pending.availableThinkingLevels.value; + } + } + if ( + pending.model === undefined && + pending.thinkingLevel === undefined && + pending.availableThinkingLevels === undefined + ) { pendingRuntimeSettings.delete(runtime.key); + } return state; } @@ -710,19 +724,54 @@ export function createAppStore() { })); } - /** Patch the card until a snapshot reaches this confirmed mutation's revision. */ - function setRuntimeModel(key: string, model: { provider: string; id: string }, settingsRevision: number): void { + /** Patch model and model-aware thinking state until a snapshot reaches this confirmed mutation's revision. */ + function setRuntimeModel( + key: string, + result: { + model: { provider: string; id: string }; + thinkingLevel: string; + availableThinkingLevels: string[]; + settingsRevision: number; + }, + ): void { const runtime = fleet().runtimes.find((candidate) => candidate.key === key); const pending = pendingRuntimeSettings.get(key) ?? {}; - if ((runtime?.settingsRevision ?? 0) >= settingsRevision) delete pending.model; - else pending.model = { value: model, settingsRevision }; - if (pending.model === undefined && pending.thinkingLevel === undefined) pendingRuntimeSettings.delete(key); - else pendingRuntimeSettings.set(key, pending); - if ((runtime?.settingsRevision ?? 0) >= settingsRevision) return; + if ((runtime?.settingsRevision ?? 0) >= result.settingsRevision) { + delete pending.model; + delete pending.thinkingLevel; + delete pending.availableThinkingLevels; + } else { + pending.model = { value: result.model, settingsRevision: result.settingsRevision }; + pending.thinkingLevel = { value: result.thinkingLevel, settingsRevision: result.settingsRevision }; + pending.availableThinkingLevels = { + value: [...result.availableThinkingLevels], + settingsRevision: result.settingsRevision, + }; + } + if ( + pending.model === undefined && + pending.thinkingLevel === undefined && + pending.availableThinkingLevels === undefined + ) { + pendingRuntimeSettings.delete(key); + } else { + pendingRuntimeSettings.set(key, pending); + } + if ((runtime?.settingsRevision ?? 0) >= result.settingsRevision) return; mutateFleet((current) => ({ ...current, runtimes: current.runtimes.map((candidate) => - candidate.key === key ? { ...candidate, state: { ...candidate.state, model } } : candidate, + candidate.key === key + ? { + ...candidate, + state: { + ...candidate.state, + model: result.model, + thinkingLevel: result.thinkingLevel, + availableThinkingLevels: [...result.availableThinkingLevels], + }, + } + : candidate, ), })); } @@ -733,8 +782,15 @@ export function createAppStore() { const pending = pendingRuntimeSettings.get(key) ?? {}; if ((runtime?.settingsRevision ?? 0) >= settingsRevision) delete pending.thinkingLevel; else pending.thinkingLevel = { value: thinkingLevel, settingsRevision }; - if (pending.model === undefined && pending.thinkingLevel === undefined) pendingRuntimeSettings.delete(key); - else pendingRuntimeSettings.set(key, pending); + if ( + pending.model === undefined && + pending.thinkingLevel === undefined && + pending.availableThinkingLevels === undefined + ) { + pendingRuntimeSettings.delete(key); + } else { + pendingRuntimeSettings.set(key, pending); + } if ((runtime?.settingsRevision ?? 0) >= settingsRevision) return; mutateFleet((current) => ({ ...current, diff --git a/packages/dashboard/src/server/runtime-pool.ts b/packages/dashboard/src/server/runtime-pool.ts index 95c6693f..30307b59 100644 --- a/packages/dashboard/src/server/runtime-pool.ts +++ b/packages/dashboard/src/server/runtime-pool.ts @@ -198,12 +198,28 @@ export class RuntimePool { handle: RuntimeHandle, provider: string, modelId: string, - ): Promise<{ provider: string; id: string; settingsRevision: number }> { + ): Promise<{ + model: { provider: string; id: string }; + thinkingLevel: string; + availableThinkingLevels: string[]; + settingsRevision: number; + }> { const model = await handle.client.setModel(provider, modelId); - handle.lastState = { ...this.fallbackState(handle), model }; + const state = await handle.client.getState(); + handle.lastState = { + ...this.fallbackState(handle), + model, + thinkingLevel: state.thinkingLevel, + availableThinkingLevels: state.availableThinkingLevels, + }; handle.settingsRevision += 1; if (this.runtimes.get(handle.key) === handle) this.scheduleFleetSnapshot(); - return { ...model, settingsRevision: handle.settingsRevision }; + return { + model, + thinkingLevel: state.thinkingLevel, + availableThinkingLevels: state.availableThinkingLevels, + settingsRevision: handle.settingsRevision, + }; } /** Apply a confirmed thinking mutation to both the child and the pool snapshot. */ @@ -673,6 +689,7 @@ export class RuntimePool { sessionId: previous?.sessionId ?? handle.key, tasks: previous?.tasks ? [...previous.tasks] : [], thinkingLevel: previous?.thinkingLevel ?? "off", + availableThinkingLevels: previous?.availableThinkingLevels ? [...previous.availableThinkingLevels] : ["off"], isStreaming: previous?.isStreaming ?? false, isRetrying: previous?.isRetrying ?? false, retryAttempt: previous?.retryAttempt ?? 0, diff --git a/packages/dashboard/src/shared/protocol.ts b/packages/dashboard/src/shared/protocol.ts index 04e34b7e..125da176 100644 --- a/packages/dashboard/src/shared/protocol.ts +++ b/packages/dashboard/src/shared/protocol.ts @@ -30,7 +30,7 @@ export interface SessionInventoryDto { export interface ArbitrationRouteDto { agent: string; model: string; - thinking: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; + thinking: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; } export interface SubagentArbitrationDto { @@ -245,6 +245,8 @@ export interface SessionStateDto { /** Current task list, atomically replaced by tasks_update events. */ tasks: SessionTaskDto[]; thinkingLevel: string; + /** Model-aware levels available for selection and cycling. */ + availableThinkingLevels: string[]; isStreaming: boolean; /** True while automatic retry classification, backoff, or execution is active. */ isRetrying?: boolean; @@ -511,7 +513,7 @@ export interface PairedDeviceDto { export interface SubagentArbiterSettingsDto { enabled?: boolean; model?: string; - thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; + thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; guidePath?: string; } diff --git a/packages/dashboard/test/client/screens.test.tsx b/packages/dashboard/test/client/screens.test.tsx index c1e20db9..6c09667a 100644 --- a/packages/dashboard/test/client/screens.test.tsx +++ b/packages/dashboard/test/client/screens.test.tsx @@ -216,7 +216,12 @@ vi.mock("../../src/client/api.js", () => ({ abort: vi.fn(async () => ({})), abortCompaction: vi.fn(async () => ({})), abortRetry: vi.fn(async () => ({})), - setModel: vi.fn(async () => ({ provider: "test", id: "m1", settingsRevision: 1 })), + setModel: vi.fn(async () => ({ + model: { provider: "test", id: "m1" }, + thinkingLevel: "off", + availableThinkingLevels: ["off"], + settingsRevision: 1, + })), setThinking: vi.fn(async () => ({ ok: true, settingsRevision: 1 })), compact: vi.fn(async () => ({})), newSession: vi.fn(async () => ({ cancelled: false })), @@ -6615,7 +6620,12 @@ describe("dashboard client regressions", () => { vi.mocked(api.fleet).mockClear(); (el.querySelector(".model-row") as HTMLButtonElement).click(); await new Promise((resolve) => setTimeout(resolve, 10)); - expect(setRuntimeModel).toHaveBeenCalledWith("k1", { provider: "test", id: "m1" }, 1); + expect(setRuntimeModel).toHaveBeenCalledWith("k1", { + model: { provider: "test", id: "m1" }, + thinkingLevel: "off", + availableThinkingLevels: ["off"], + settingsRevision: 1, + }); expect(vi.mocked(api.fleet)).not.toHaveBeenCalled(); }); diff --git a/packages/dashboard/test/client/store.test.ts b/packages/dashboard/test/client/store.test.ts index e214f377..454aec31 100644 --- a/packages/dashboard/test/client/store.test.ts +++ b/packages/dashboard/test/client/store.test.ts @@ -2077,7 +2077,12 @@ describe("app store hydration", () => { const store = await makeStartedStore(); const hydrate = store.hydrateSession("mixed-freshness"); - store.setRuntimeModel("mixed-freshness", { provider: "test", id: "new-model" }, 1); + store.setRuntimeModel("mixed-freshness", { + model: { provider: "test", id: "new-model" }, + thinkingLevel: "off", + availableThinkingLevels: ["off"], + settingsRevision: 1, + }); const preBarrierSnapshot = runtimeSnapshot("mixed-freshness", true); preBarrierSnapshot.state.model = { provider: "test", id: "new-model" }; preBarrierSnapshot.settingsRevision = 1; @@ -2416,7 +2421,12 @@ describe("fleet snapshot and inventory store foundation", () => { const refresh = store.refreshFleet(); store.upsertRuntime(fleetSnapshot("created")); - store.setRuntimeModel("created", { provider: "test", id: "new-model" }, 1); + store.setRuntimeModel("created", { + model: { provider: "test", id: "new-model" }, + thinkingLevel: "off", + availableThinkingLevels: ["off"], + settingsRevision: 1, + }); store.setRuntimeThinkingLevel("created", "high", 2); delayed.resolve({ runtimes: [fleetSnapshot("stale")], diskSessions: [] }); await refresh; @@ -2434,7 +2444,12 @@ describe("fleet snapshot and inventory store foundation", () => { vi.mocked(api.fleet).mockResolvedValueOnce({ runtimes: [initial], diskSessions: [] }); const store = await makeStartedStore(); - store.setRuntimeModel("live", { provider: "test", id: "new-model" }, 1); + store.setRuntimeModel("live", { + model: { provider: "test", id: "new-model" }, + thinkingLevel: "off", + availableThinkingLevels: ["off"], + settingsRevision: 1, + }); store.setRuntimeThinkingLevel("live", "high", 2); const stale = fleetSnapshot("live"); stale.state.model = { provider: "test", id: "old-model" }; @@ -2467,7 +2482,12 @@ describe("fleet snapshot and inventory store foundation", () => { vi.mocked(api.fleet).mockResolvedValueOnce({ runtimes: [initial], diskSessions: [] }); const store = await makeStartedStore(); - store.setRuntimeModel("live", { provider: "test", id: "confirmed-model" }, 1); + store.setRuntimeModel("live", { + model: { provider: "test", id: "confirmed-model" }, + thinkingLevel: "off", + availableThinkingLevels: ["off"], + settingsRevision: 1, + }); store.setRuntimeThinkingLevel("live", "high", 2); const stale = fleetSnapshot("live"); stale.state.model = { provider: "test", id: "old-model" }; @@ -2497,7 +2517,12 @@ describe("fleet snapshot and inventory store foundation", () => { vi.mocked(api.fleet).mockResolvedValueOnce({ runtimes: [initial], diskSessions: [] }); const store = await makeStartedStore(); - store.setRuntimeModel("live", { provider: "test", id: "new-model" }, 1); + store.setRuntimeModel("live", { + model: { provider: "test", id: "new-model" }, + thinkingLevel: "off", + availableThinkingLevels: ["off"], + settingsRevision: 1, + }); store.setRuntimeThinkingLevel("live", "high", 2); const later = fleetSnapshot("live"); later.state.model = { provider: "test", id: "later-model" }; diff --git a/packages/dashboard/test/runtime-pool.test.ts b/packages/dashboard/test/runtime-pool.test.ts index e95fc70c..8a43081d 100644 --- a/packages/dashboard/test/runtime-pool.test.ts +++ b/packages/dashboard/test/runtime-pool.test.ts @@ -24,6 +24,7 @@ export function makeFakeClient() { sessionId: "s1", tasks: [], thinkingLevel: "medium", + availableThinkingLevels: ["off", "medium", "high"], isStreaming: false, isRetrying: false, retryAttempt: 0, @@ -249,8 +250,9 @@ describe("RuntimePool", () => { pool.onFleetSnapshot(snapshots); await expect(pool.setModel(handle, "test", "new-model")).resolves.toEqual({ - provider: "test", - id: "new-model", + model: { provider: "test", id: "new-model" }, + thinkingLevel: "medium", + availableThinkingLevels: ["off", "medium", "high"], settingsRevision: 1, }); await expect(pool.setThinkingLevel(handle, "high")).resolves.toEqual({ diff --git a/packages/dashboard/test/server.test.ts b/packages/dashboard/test/server.test.ts index fa304d86..2af4346c 100644 --- a/packages/dashboard/test/server.test.ts +++ b/packages/dashboard/test/server.test.ts @@ -1011,8 +1011,9 @@ describe("dashboard server — fleet and runtimes", () => { }); expect(model.status).toBe(200); await expect(model.json()).resolves.toEqual({ - provider: "test", - id: "new-model", + model: { provider: "test", id: "new-model" }, + thinkingLevel: "medium", + availableThinkingLevels: ["off", "medium", "high"], settingsRevision: 1, }); diff --git a/packages/semantic-search/.claude-plugin/plugin.json b/packages/semantic-search/.claude-plugin/plugin.json index 22d257d0..82676fe4 100644 --- a/packages/semantic-search/.claude-plugin/plugin.json +++ b/packages/semantic-search/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "semantic-search", "description": "Semantic codebase search — natural language queries over code and docs using embeddings, tree-sitter parsing, and POEM multi-signal ranking", - "version": "2.61.0", + "version": "2.61.1", "author": { "name": "Drew Brereton" }, diff --git a/packages/semantic-search/package.json b/packages/semantic-search/package.json index abdbb239..0bc0f646 100644 --- a/packages/semantic-search/package.json +++ b/packages/semantic-search/package.json @@ -1,6 +1,6 @@ { "name": "@dreb/semantic-search", - "version": "2.61.0", + "version": "2.61.1", "description": "Semantic codebase search engine with embedding-based ranking and MCP server", "publishConfig": { "access": "public" diff --git a/packages/telegram/package.json b/packages/telegram/package.json index 57b41945..451577ca 100644 --- a/packages/telegram/package.json +++ b/packages/telegram/package.json @@ -1,6 +1,6 @@ { "name": "@dreb/telegram", - "version": "2.61.0", + "version": "2.61.1", "description": "Telegram bot frontend for dreb coding agent", "license": "MIT", "type": "module", diff --git a/packages/telegram/src/commands/agent.ts b/packages/telegram/src/commands/agent.ts index 67fbd74d..37bb12b3 100644 --- a/packages/telegram/src/commands/agent.ts +++ b/packages/telegram/src/commands/agent.ts @@ -188,7 +188,7 @@ export async function cmdThinking(ctx: Context, userState: UserState, args: stri } const level = args.trim().toLowerCase(); - const valid = ["off", "minimal", "low", "medium", "high"]; + const valid = ["off", "minimal", "low", "medium", "high", "xhigh", "max"]; if (!valid.includes(level)) { await safeSend(ctx.api, chatId, `Invalid level. Options: ${valid.join(", ")}`); return; diff --git a/packages/telegram/test/commands.test.ts b/packages/telegram/test/commands.test.ts index 2a4ee26d..d703ab27 100644 --- a/packages/telegram/test/commands.test.ts +++ b/packages/telegram/test/commands.test.ts @@ -29,7 +29,7 @@ vi.mock("node:os", async () => { import { statSync } from "node:fs"; import { homedir } from "node:os"; -import { cmdStats } from "../src/commands/agent.js"; +import { cmdStats, cmdThinking } from "../src/commands/agent.js"; // Import after mock setup import { cmdNew } from "../src/commands/core.js"; @@ -465,3 +465,23 @@ describe("cmdStats", () => { expect(mockSafeSend).toHaveBeenCalledWith(expect.anything(), 100, expect.stringContaining("RPC timeout")); }); }); + +describe("cmdThinking", () => { + beforeEach(() => vi.clearAllMocks()); + + it.each(["xhigh", "max"])("accepts %s", async (level) => { + const bridge = { isAlive: true, setThinkingLevel: vi.fn().mockResolvedValue(undefined) } as any; + await cmdThinking(createMockContext(), createUserState({ bridge }), level); + expect(bridge.setThinkingLevel).toHaveBeenCalledWith(level); + }); + + it("lists the complete scale for invalid input", async () => { + const bridge = { isAlive: true, setThinkingLevel: vi.fn() } as any; + await cmdThinking(createMockContext(), createUserState({ bridge }), "ultra"); + expect(mockSafeSend).toHaveBeenCalledWith( + expect.anything(), + 100, + "Invalid level. Options: off, minimal, low, medium, high, xhigh, max", + ); + }); +}); diff --git a/packages/tui/package.json b/packages/tui/package.json index 963fcef3..8cc030fb 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@dreb/tui", - "version": "2.61.0", + "version": "2.61.1", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js",