From 0d64e5a8314a57c746083d0804c741c4ef3a6851 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 17:23:28 -0700 Subject: [PATCH 1/6] feat(provider): adapt deepseek-v4-flash wire params and family detection DeepSeek V4 Flash is the exception to the historical DeepSeek 400 rule: its encoder keeps every turn's reasoning when tools are present, so stripping it rendered prior tool turns empty and the model stopped thinking. Add the deepseek-v4-flash family (provider-family.ts) and apply the model-card wire shape for that family in the openai-compatible adapter: chat_template_kwargs.thinking, effort folded onto low/high/max, agentic temperature 1.0 / top_p 0.95, and stream_options.include_usage. The family regex keeps other DeepSeek models on the old strip behavior. Added 6 unit tests (15 total pass). CL-10241. --- .../openai-compatible-adapter.test.ts | 73 +++++++++++++++++++ src/provider/openai-compatible-adapter.ts | 69 ++++++++++++++++-- src/subagent/provider-family.ts | 18 +++++ 3 files changed, 155 insertions(+), 5 deletions(-) diff --git a/src/provider/openai-compatible-adapter.test.ts b/src/provider/openai-compatible-adapter.test.ts index 518ff29b5..23b8706b1 100644 --- a/src/provider/openai-compatible-adapter.test.ts +++ b/src/provider/openai-compatible-adapter.test.ts @@ -164,3 +164,76 @@ describe("openai-compatible adapter reasoning_content handling", () => { expect(assistant?.["reasoning_content"]).toBe("ponder"); }); }); + +describe("openai-compatible adapter DeepSeek V4 Flash wire params", () => { + const V4 = "deepseek-ai/DeepSeek-V4-Flash-0731"; + const history: ConversationTurn[] = [ + { role: "user", content: [{ type: "text", text: "hi" }] }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "ponder" }, + { type: "text", text: "hello" }, + ], + }, + { role: "user", content: [{ type: "text", text: "again" }] }, + ] as unknown as ConversationTurn[]; + + function v4Body( + providerOptions?: Record, + model = V4, + ): Record { + const adapter = createOpenAICompatibleAdapter(source); + const built = adapter.buildRequest(history, model, { + maxTokens: 100, + ...(providerOptions !== undefined ? { providerOptions } : {}), + } as InferenceOptions); + return JSON.parse(built.body) as Record; + } + + test("turns thinking on and applies agentic sampling at high effort", () => { + const body = v4Body({ reasoning_effort: "high" }); + expect(body["chat_template_kwargs"]).toEqual({ thinking: true }); + expect(body["reasoning_effort"]).toBe("high"); + expect(body["temperature"]).toBe(1); + expect(body["top_p"]).toBe(0.95); + expect(body["stream_options"]).toEqual({ include_usage: true }); + }); + + test("folds Corbits' ladder onto low/high/max", () => { + expect(v4Body({ reasoning_effort: "minimal" })["reasoning_effort"]).toBe("low"); + expect(v4Body({ reasoning_effort: "medium" })["reasoning_effort"]).toBe("high"); + expect(v4Body({ reasoning_effort: "max" })["reasoning_effort"]).toBe("max"); + }); + + test("effort none sends chat mode explicitly and no effort", () => { + const body = v4Body({ reasoning_effort: "none" }); + expect(body["chat_template_kwargs"]).toEqual({ thinking: false }); + expect("reasoning_effort" in body).toBe(false); + expect("top_p" in body).toBe(false); + }); + + test("an explicitly configured temperature/top_p wins", () => { + const body = v4Body({ reasoning_effort: "high", temperature: 0.6, top_p: 0.9 }); + expect(body["temperature"]).toBe(0.6); + expect(body["top_p"]).toBe(0.9); + }); + + test("replays reasoning_content on prior assistant turns", () => { + const msgs = v4Body({ reasoning_effort: "high" })["messages"] as Record< + string, + unknown + >[]; + const assistant = msgs.find((m) => m["role"] === "assistant"); + expect(assistant?.["reasoning_content"]).toBe("ponder"); + }); + + test("other DeepSeek models keep the old strip and get no extra params", () => { + const body = v4Body({ reasoning_effort: "high" }, "deepseek-v3.2"); + expect("chat_template_kwargs" in body).toBe(false); + expect("top_p" in body).toBe(false); + const msgs = body["messages"] as Record[]; + const assistant = msgs.find((m) => m["role"] === "assistant"); + expect("reasoning_content" in defined(assistant)).toBe(false); + }); +}); diff --git a/src/provider/openai-compatible-adapter.ts b/src/provider/openai-compatible-adapter.ts index d869af801..679a914ca 100644 --- a/src/provider/openai-compatible-adapter.ts +++ b/src/provider/openai-compatible-adapter.ts @@ -45,11 +45,19 @@ export function createOpenAICompatibleAdapter( const providerOptions = options.providerOptions; const hasProviderOptions = providerOptions !== undefined && Object.keys(providerOptions).length > 0; - // DeepSeek returns HTTP 400 if `reasoning_content` appears in input messages, - // whereas the base adapter emits it for any model with thinking enabled. - const stripReasoning = model.toLowerCase().includes("deepseek"); - needsDeepSeekPatch = stripReasoning; - if (!hasProviderOptions && !stripReasoning) return ensureAccept(built); + // DeepSeek's hosted API historically returned HTTP 400 if + // `reasoning_content` appeared in input messages, whereas the base adapter + // emits it for any model with thinking enabled. DeepSeek V4 Flash is the + // exception: its encoder keeps every turn's reasoning when tools are + // present, so stripping it renders each prior tool turn as an empty + // thinking response and the model copies that and stops thinking. + const v4Flash = isDeepSeekV4Flash(model); + const stripReasoning = + model.toLowerCase().includes("deepseek") && !v4Flash; + needsDeepSeekPatch = model.toLowerCase().includes("deepseek"); + const v4FlashParams = v4Flash; + if (!hasProviderOptions && !stripReasoning && !v4FlashParams) + return ensureAccept(built); const body = JSON.parse(built.body) as Record; if (hasProviderOptions) Object.assign(body, providerOptions); @@ -59,6 +67,7 @@ export function createOpenAICompatibleAdapter( delete (msg as Record)["reasoning_content"]; } } + if (v4FlashParams) applyDeepSeekV4FlashParams(body); const merged: BuiltRequest = { ...built, body: JSON.stringify(body) }; return ensureAccept(merged); }; @@ -73,3 +82,53 @@ export function createOpenAICompatibleAdapter( return { ...base, buildRequest, parseResponse }; } + +/** DeepSeek V4 Flash on any OpenAI-compatible host (SGLang, vLLM, gateway). */ +export function isDeepSeekV4Flash(model: string): boolean { + return /(^|\/)deepseek-v4-flash/i.test(model.trim()); +} + +// DeepSeek V4 understands exactly three reasoning_effort levels (low, high, +// max); its reference encoder asserts on anything else. Corbits' generic +// ladder (minimal/low/medium/high/xhigh/max) is folded onto those three. +const DSV4_EFFORT: Record = { + minimal: "low", + low: "low", + medium: "high", + high: "high", + xhigh: "max", + max: "max", +}; + +/** + * Wire shape the DeepSeek V4 Flash model card asks for, applied to an + * already-built Chat Completions body: + * - thinking is driven by `chat_template_kwargs.thinking` (SGLang and vLLM + * both read it); `reasoning_effort` "none"/"off" or absent means chat mode. + * - effort folds onto low/high/max. + * - thinking mode samples at temperature 1.0 / top_p 0.95 (agentic); an + * explicitly configured temperature/top_p still wins. + * - stream usage is requested so token counts are observable. + */ +export function applyDeepSeekV4FlashParams(body: Record): void { + const raw = body["reasoning_effort"]; + const effort = typeof raw === "string" ? raw.toLowerCase() : undefined; + const thinking = + effort !== undefined && effort !== "none" && effort !== "off"; + const existingKwargs = + body["chat_template_kwargs"] !== null && + typeof body["chat_template_kwargs"] === "object" + ? (body["chat_template_kwargs"] as Record) + : {}; + body["chat_template_kwargs"] = { ...existingKwargs, thinking }; + if (thinking) { + body["reasoning_effort"] = DSV4_EFFORT[effort] ?? "high"; + if (body["temperature"] === undefined) body["temperature"] = 1.0; + if (body["top_p"] === undefined) body["top_p"] = 0.95; + } else { + delete body["reasoning_effort"]; + } + if (body["stream"] === true && body["stream_options"] === undefined) { + body["stream_options"] = { include_usage: true }; + } +} diff --git a/src/subagent/provider-family.ts b/src/subagent/provider-family.ts index 8a42497e4..97388579b 100644 --- a/src/subagent/provider-family.ts +++ b/src/subagent/provider-family.ts @@ -83,8 +83,25 @@ export function isGptProvider(input: { return false; } +/** + * True when the model id is DeepSeek V4 Flash (any provider: self-hosted + * SGLang/vLLM or a gateway). Scoped to the Flash cell on purpose: the + * residual it unlocks was tuned against that model only, so other DeepSeek + * ids keep the default policy. + */ +export function isDeepSeekV4FlashProvider(input: { + providerName: string; + model?: string; +}): boolean { + return ( + input.model !== undefined && + /(^|\/)deepseek-v4-flash/i.test(input.model.trim()) + ); +} + /** Model families the shared directors branch on via ModelFamilyPolicy. */ export type ModelFamily = + | "deepseek-v4-flash" | "grok" | "kimi" | "muse" @@ -103,6 +120,7 @@ export function detectModelFamily(input: { providerName: string; model?: string; }): ModelFamily { + if (isDeepSeekV4FlashProvider(input)) return "deepseek-v4-flash"; if (isXaiGrokLeafProvider(input)) return "grok"; if (isKimiLeafProvider(input)) return "kimi"; if (isMuseSparkLeafProvider(input)) return "muse"; From ace7edb73956acd546e6ec697e0855e9f25ad312 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 17:26:57 -0700 Subject: [PATCH 2/6] feat(agent): bake in winning DeepSeek V4 Flash prompt/config (d1+slim, coder=low) Make the winning round-4 config (c) a built-in default for the deepseek-v4-flash family: d1_primary.txt as the orchestrator's appended prompt residual, per-role leaf bodies (body_coder/body_reviewer) via the new ModelFamilyPolicy.leafRoleBody field, and the explorer leaf residual (g2_leaf). The coder sub-agent pins its effort to low as the winning default, with an explicitly configured profile effort still winning. CORBITS_EXP_DSV4_* eval hooks remain wired (an env override wins over a built-in) but are commented as removed in the CL-10242 follow-up. Added 5 family-residual tests. CL-10242. --- src/agent/model-family-policy.test.ts | 51 ++++++++++++ src/agent/model-family-policy.ts | 109 ++++++++++++++++++++++++++ src/subagent/agent-fleet.ts | 20 ++++- src/subagent/run.ts | 9 ++- 4 files changed, 183 insertions(+), 6 deletions(-) diff --git a/src/agent/model-family-policy.test.ts b/src/agent/model-family-policy.test.ts index 7c6949727..2e4493337 100644 --- a/src/agent/model-family-policy.test.ts +++ b/src/agent/model-family-policy.test.ts @@ -388,4 +388,55 @@ describe("resolveModelFamilyPolicy", () => { } } }); + + describe("deepseek-v4-flash bake-in (CL-10242)", () => { + function v4(input: { + orchestrator?: boolean; + directorId?: string; + } = {}) { + return resolveModelFamilyPolicy({ + providerName: "vast", + model: "deepseek-ai/DeepSeek-V4-Flash-0731", + ...(input.orchestrator !== undefined ? { orchestrator: input.orchestrator } : {}), + ...(input.directorId !== undefined ? { directorId: input.directorId } : {}), + }); + } + + test("orchestrators get the d1 primary residual, no primary card or leaf body", () => { + const p = v4({ orchestrator: true }); + expect(p.family).toBe("deepseek-v4-flash"); + expect(p.promptResidual).toBeDefined(); + expect(p.promptResidual).toContain("DeepSeek V4 Flash"); + expect(p.leafRoleBody).toBeUndefined(); + }); + + test("coder leaves get the tuned leaf role body, no residual", () => { + const p = v4({ orchestrator: false, directorId: "coder" }); + expect(p.family).toBe("deepseek-v4-flash"); + expect(p.leafRoleBody).toBeDefined(); + expect(p.leafRoleBody).toContain("Coder"); + expect(p.promptResidual).toBeUndefined(); + }); + + test("reviewer leaves get the tuned reviewer body", () => { + const p = v4({ orchestrator: false, directorId: "reviewer" }); + expect(p.leafRoleBody).toBeDefined(); + expect(p.leafRoleBody).toContain("Reviewer"); + expect(p.promptResidual).toBeUndefined(); + }); + + test("explorer leaves get the tuned leaf residual only", () => { + const p = v4({ orchestrator: false, directorId: "explorer" }); + expect(p.promptResidual).toBeDefined(); + expect(p.promptResidual).toContain("DeepSeek V4 Flash"); + expect(p.leafRoleBody).toBeUndefined(); + }); + + test("a leaf without a tuned director id gets the default policy, no body/residual", () => { + const p = v4({ orchestrator: false }); + expect(p.family).toBe("deepseek-v4-flash"); + expect(p.leafRoleBody).toBeUndefined(); + expect(p.promptResidual).toBeUndefined(); + }); + }); }); diff --git a/src/agent/model-family-policy.ts b/src/agent/model-family-policy.ts index 5301e8bed..738b35fd0 100644 --- a/src/agent/model-family-policy.ts +++ b/src/agent/model-family-policy.ts @@ -55,6 +55,12 @@ export interface ModelFamilyPolicy { * disturb the cached prompt prefix. Undefined for families that need none. */ promptResidual?: string | undefined; + /** + * Replaces the director body (formatDirectorSystemPrompt) on a leaf worker + * prompt for families with a tuned slim body for that role. Undefined keeps + * the shared director body. + */ + leafRoleBody?: string | undefined; } const DEFAULT_WRAP_UP_NUDGE_TEXT = @@ -200,10 +206,84 @@ const ASTRA_POLICY: Omit = { promptResidual: ASTRA_PROMPT_RESIDUAL, }; +// DeepSeek V4 Flash build-in (CL-10242) — the winning round-4 config (c): +// d1+slim-high bodies, dispatch=max + coder=low. Text is single-sourced here +// (policy owns prompt data, matching the muse/grok/claude/gpt precedent) from +// the handoff's winning prompt files: +// d1_primary.txt -> the orchestrator's appended prompt residual +// (DSV4_D1_PRIMARY_RESIDUAL, promptResidual seam) +// body_coder.txt -> the coder leaf's role body (leafRoleBody) +// body_reviewer.txt-> the reviewer leaf's role body (leafRoleBody) +// g2_leaf.txt -> the explorer leaf's residual (promptResidual) +// A CORBITS_EXP_DSV4_* override still wins when set (eval-only; the removal +// of those hooks is CL-10242's follow-up deliverable). +export const DSV4_D1_PRIMARY_RESIDUAL = ` +Operating notes (DeepSeek V4 Flash): +- Your context is the expensive one: workers do the reading, building, and checking. Read only what you need to route and to name files in a brief (one glob or grep, or one or two reads); do not read code to plan the fix yourself. +- Route: any code change (fix, feature, refactor, even one line) goes to coder; verification of that change goes to reviewer after the coder reports; codebase questions that need more than two reads go to explorer; a question answerable from one or two reads you answer directly. Set \`agent\` to the id directly; do not call search_agents. +- Write each brief so the worker can start without rediscovering anything: goal (copy the user's requirements verbatim, including exact formats, names, and messages), files (repo-relative paths to change and to leave alone), constraints in do_not (no edits to tests or locked files, no new deps, no extra tests or files unless asked), success_criteria (observable checks, one per requirement), and the exact test command. +- After the coder reports, spawn reviewer with the same success_criteria, the test command, and the coder's changed paths. If the reviewer reports a defect, send it back to the same coder with send_input, then have the reviewer re-check once. +- Always call wait_agents on a worker you spawned before ending your turn; never end the turn with a worker still running. +- Do not edit files yourself and do not re-read changed files or re-run tests after a report: the reviewer's verdict is the verification. +- Files go through the file tools, not the shell: read, grep, glob. glob refuses ** unless \`path\` is a subdirectory: use {"path": "src", "pattern": "**/*.ts"}, never {"pattern": "src/**/*.ts"}. +- Final reply: what changed (files), who verified it and how, anything left open, in five lines or fewer. Do not restate the request. +`.trim(); + +export const DSV4_BODY_CODER = ` +Identity: agent id \`coder\`. You are Coder, the builder for Corbits Code, running on DeepSeek V4 Flash. You implement one brief, then report. A separate reviewer verifies your work. + +- Start from the brief: it names the goal, the files, the constraints, the success criteria, and the test command. Read those files, make the change, run the test command once, report. +- Read a file before editing it, unless you just created or edited it. Use edit for targeted changes and write for new files. +- Use read, not cat/head/tail in bash; read with no offset/limit returns the whole file, so read each file once. Use grep, not shell grep or rg. Use glob, not find or ls. +- glob refuses ** unless \`path\` is a subdirectory: {"path": "src", "pattern": "**/*.ts"} works; {"pattern": "src/**/*.ts"} is refused. +- Use bash for tests, builds, and programs. A bash result that starts with \`exit code N\` failed: read it and fix the cause before moving on. +- Make the smallest change at the root cause. Keep public signatures and existing behavior the brief does not mention. Add no tests, files, refactors, or docs the brief did not ask for. +- Build structured output (JSON, CSV, YAML) with the language's serializer (JSON.stringify, json.dumps), never by string interpolation. +- Do not re-verify beyond the brief's test command and one direct check per success criterion; the reviewer does the rest. +- Report with the Report envelope (Summary / Findings / Blockers / Paths). In Findings, give each success criterion as pass/fail with the command and its exit status. Paths lists every file you changed. +`.trim(); + +export const DSV4_BODY_REVIEWER = ` +Identity: agent id \`reviewer\`. You are Reviewer, the verifier for Corbits Code, running on DeepSeek V4 Flash. You check a change against the brief's success criteria. You never edit product code. + +- Run the brief's test command once. Then check each success criterion with the smallest direct evidence: read the changed lines, and run one targeted command (call the function or CLI with python3 -c / node -e) for behavior the tests do not cover, including exact output formats. +- Report a defect only with evidence: path:line, the input, expected vs actual. +- Do not add files to the repo; use inline commands and leave the working tree as you found it. +- Use read, grep, and glob for files, not shell cat/grep/find. glob refuses ** unless \`path\` is a subdirectory: {"path": "src", "pattern": "**/*.ts"}. +- A bash result that starts with \`exit code N\` failed: say what failed. +- Report with the Report envelope (Summary / Findings / Blockers / Paths). Summary: PASS or FAIL in one line. Findings: one line per success criterion (pass/fail + evidence), then any defects. Keep the whole report under 15 lines. +`.trim(); + +export const DSV4_G2_LEAF_RESIDUAL = ` +Operating notes (DeepSeek V4 Flash): +- Files go through the file tools, not the shell: read to view a file, grep to search contents, glob to find files. bash is for running tests, builds, and programs only. +- glob refuses ** unless \`path\` is a subdirectory. Put the directory in \`path\`, never at the front of the pattern: {"path": "src", "pattern": "**/*.ts"} works; {"pattern": "src/**/*.ts"}, {"pattern": "tests/**/*"} and {"pattern": "**/*"} are refused. At the root, survey with {"pattern": "*.*"} and {"pattern": "*/*"}. +- read takes a file path, never a directory; list a directory with glob ({"path": "src", "pattern": "*"}). Do not guess file names: glob or grep first. +- read with no offset/limit returns the whole file; read each file once and do not page past its end. +- Do exactly what the brief asks. No extra tests, files, refactors, or docs unless the brief requires them; when the brief or the operator says no tests, write none. +- Produce structured output (JSON, CSV, YAML) with the language's serializer (JSON.stringify, json.dumps), never by string interpolation. +- Stay proportional: a few targeted checks cover a small change. Once the success criteria are verified, stop; do not build harnesses for exotic inputs the brief never mentions. +- Run the project's own test command once after your last edit, then write the report. +`.trim(); + +// EXPERIMENT HOOK (removed in the CL-10242 follow-up): lets the eval swap the +// baked-in deepseek-v4-flash residual/body text without rebuilding. A set env +// var wins over the built-in constant; the removal task deletes these and the +// __expResidual reader together. +import { readFileSync as __expReadFileSync } from "node:fs"; +function __expResidual(kind: string): string | undefined { + const path = process.env[`CORBITS_EXP_DSV4_${kind}`]; + if (path === undefined || path === "") return undefined; + const text = __expReadFileSync(path, "utf8").trim(); + return text.length > 0 ? text : undefined; +} + export function resolveModelFamilyPolicy(input: { providerName: string; model?: string; orchestrator?: boolean; + /** Leaf director id (coder, reviewer, explorer, …) for role-tuned families. */ + directorId?: string; }): ModelFamilyPolicy { const family = detectModelFamily(input); const orchestrator = input.orchestrator === true; @@ -240,6 +320,35 @@ export function resolveModelFamilyPolicy(input: { // Primary and leaf alike, like gpt: no orchestrator carve-out. Generic // gpt prompts are byte-identical — only astra carries the residual. return { family, ...ASTRA_POLICY }; + case "deepseek-v4-flash": { + // Winning config (c) bake-in (CL-10242): d1+slim-high bodies, + // dispatch=max + coder=low. Orchestrators get the slim primary card + // (primaryRole) and coder/reviewer/explorer leaves get their tuned + // role bodies plus the leaf residual. A CORBITS_EXP_DSV4_* env override + // wins when set (eval-only hook; removal is the CL-10242 follow-up). + const directorId = input.directorId; + return { + family, + ...DEFAULT_POLICY, + // Winning config (c): d1_primary.txt is the orchestrator's appended + // prompt residual (the PRIMARY hook), the explorer leaf carries the + // g2_leaf residual (LEAF_EXPLORER), and coder/reviewer leaves carry + // their tuned role bodies (BODY_CODER / BODY_REVIEWER). The slim + // primary card (primaryRole, ROLE hook) is NOT part of the winning + // set — it stays unset so orchestrators keep the shared Dispatch card. + promptResidual: orchestrator + ? (__expResidual("PRIMARY") ?? DSV4_D1_PRIMARY_RESIDUAL) + : directorId === "explorer" + ? (__expResidual("LEAF_EXPLORER") ?? DSV4_G2_LEAF_RESIDUAL) + : undefined, + leafRoleBody: + !orchestrator && directorId === "coder" + ? (__expResidual(`BODY_CODER`) ?? DSV4_BODY_CODER) + : !orchestrator && directorId === "reviewer" + ? (__expResidual(`BODY_REVIEWER`) ?? DSV4_BODY_REVIEWER) + : undefined, + }; + } default: return { family: "default", ...DEFAULT_POLICY }; } diff --git a/src/subagent/agent-fleet.ts b/src/subagent/agent-fleet.ts index f1701fb33..10f309420 100644 --- a/src/subagent/agent-fleet.ts +++ b/src/subagent/agent-fleet.ts @@ -67,6 +67,7 @@ import { type ReasoningEffort, } from "../provider/reasoning-effort.js"; import type { AgentProfile, CapabilityFilter } from "../agent/profiles.js"; +import { isDeepSeekV4FlashProvider } from "./provider-family.js"; import { DEFAULT_KNOWN_ENGINES, formatCapabilityUnavailable, @@ -1174,11 +1175,24 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { settings?.providers[provider.providerName], catalog?.find((entry) => entry.name === provider.providerName), ); + // DeepSeek V4 Flash bake-in (CL-10242): the winning round-4 config + // pinned the coder leaf to low. Dispatch (orchestrator) effort is set + // by the parent, not here. Only the coder role gets the built-in low + // default — an explicitly configured effort pin on the profile still + // wins — and all other workers keep their role-default cascade. + const effortPin = + resolved.effortPin ?? + (!orchestrator && + resolved.directorId === "coder" && + isDeepSeekV4FlashProvider({ + providerName: provider.providerName, + model: provider.model, + }) + ? "low" + : undefined); const effort = resolveEffortForRole({ orchestrator, - ...(resolved.effortPin !== undefined - ? { pin: resolved.effortPin } - : {}), + ...(effortPin !== undefined ? { pin: effortPin } : {}), ...(resolved.roleDefault !== undefined ? { roleDefault: resolved.roleDefault } : {}), diff --git a/src/subagent/run.ts b/src/subagent/run.ts index bec36a337..10ab3ebfe 100644 --- a/src/subagent/run.ts +++ b/src/subagent/run.ts @@ -831,6 +831,9 @@ async function runSubAgentInner( providerName: params.provider.providerName, model: params.provider.model, orchestrator: params.orchestrator === true, + ...(params.directorId !== undefined + ? { directorId: params.directorId } + : {}), }); const skillDirs = [...(params.skillDirs ?? [])]; // CL-9010: reuse the dispatcher's catalog when the lane shares its cwd; @@ -1193,10 +1196,10 @@ async function runSubAgentInner( skillDirs, }) : undefined; + const roleBody = + modelFamilyPolicy.leafRoleBody ?? params.systemPromptRole; const extensions = [ - ...(params.systemPromptRole !== undefined - ? [params.systemPromptRole] - : []), + ...(roleBody !== undefined ? [roleBody] : []), ...(attachedSection !== undefined ? [attachedSection] : []), ]; const toolProfile = toolProfileForModel(params.provider); From d8c63bd58ca7ebf37200e0325ed6d94fe69d9734 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 19:53:51 -0700 Subject: [PATCH 3/6] style(agent): format baked-in prompt config --- src/agent/model-family-policy.test.ts | 18 ++++++++++++------ src/provider/openai-compatible-adapter.test.ts | 14 +++++++++++--- src/provider/openai-compatible-adapter.ts | 7 ++++--- src/subagent/run.ts | 3 +-- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/agent/model-family-policy.test.ts b/src/agent/model-family-policy.test.ts index 2e4493337..110105c63 100644 --- a/src/agent/model-family-policy.test.ts +++ b/src/agent/model-family-policy.test.ts @@ -390,15 +390,21 @@ describe("resolveModelFamilyPolicy", () => { }); describe("deepseek-v4-flash bake-in (CL-10242)", () => { - function v4(input: { - orchestrator?: boolean; - directorId?: string; - } = {}) { + function v4( + input: { + orchestrator?: boolean; + directorId?: string; + } = {}, + ) { return resolveModelFamilyPolicy({ providerName: "vast", model: "deepseek-ai/DeepSeek-V4-Flash-0731", - ...(input.orchestrator !== undefined ? { orchestrator: input.orchestrator } : {}), - ...(input.directorId !== undefined ? { directorId: input.directorId } : {}), + ...(input.orchestrator !== undefined + ? { orchestrator: input.orchestrator } + : {}), + ...(input.directorId !== undefined + ? { directorId: input.directorId } + : {}), }); } diff --git a/src/provider/openai-compatible-adapter.test.ts b/src/provider/openai-compatible-adapter.test.ts index 23b8706b1..41ec48e69 100644 --- a/src/provider/openai-compatible-adapter.test.ts +++ b/src/provider/openai-compatible-adapter.test.ts @@ -201,8 +201,12 @@ describe("openai-compatible adapter DeepSeek V4 Flash wire params", () => { }); test("folds Corbits' ladder onto low/high/max", () => { - expect(v4Body({ reasoning_effort: "minimal" })["reasoning_effort"]).toBe("low"); - expect(v4Body({ reasoning_effort: "medium" })["reasoning_effort"]).toBe("high"); + expect(v4Body({ reasoning_effort: "minimal" })["reasoning_effort"]).toBe( + "low", + ); + expect(v4Body({ reasoning_effort: "medium" })["reasoning_effort"]).toBe( + "high", + ); expect(v4Body({ reasoning_effort: "max" })["reasoning_effort"]).toBe("max"); }); @@ -214,7 +218,11 @@ describe("openai-compatible adapter DeepSeek V4 Flash wire params", () => { }); test("an explicitly configured temperature/top_p wins", () => { - const body = v4Body({ reasoning_effort: "high", temperature: 0.6, top_p: 0.9 }); + const body = v4Body({ + reasoning_effort: "high", + temperature: 0.6, + top_p: 0.9, + }); expect(body["temperature"]).toBe(0.6); expect(body["top_p"]).toBe(0.9); }); diff --git a/src/provider/openai-compatible-adapter.ts b/src/provider/openai-compatible-adapter.ts index 679a914ca..2538f386a 100644 --- a/src/provider/openai-compatible-adapter.ts +++ b/src/provider/openai-compatible-adapter.ts @@ -52,8 +52,7 @@ export function createOpenAICompatibleAdapter( // present, so stripping it renders each prior tool turn as an empty // thinking response and the model copies that and stops thinking. const v4Flash = isDeepSeekV4Flash(model); - const stripReasoning = - model.toLowerCase().includes("deepseek") && !v4Flash; + const stripReasoning = model.toLowerCase().includes("deepseek") && !v4Flash; needsDeepSeekPatch = model.toLowerCase().includes("deepseek"); const v4FlashParams = v4Flash; if (!hasProviderOptions && !stripReasoning && !v4FlashParams) @@ -110,7 +109,9 @@ const DSV4_EFFORT: Record = { * explicitly configured temperature/top_p still wins. * - stream usage is requested so token counts are observable. */ -export function applyDeepSeekV4FlashParams(body: Record): void { +export function applyDeepSeekV4FlashParams( + body: Record, +): void { const raw = body["reasoning_effort"]; const effort = typeof raw === "string" ? raw.toLowerCase() : undefined; const thinking = diff --git a/src/subagent/run.ts b/src/subagent/run.ts index 10ab3ebfe..7533224dc 100644 --- a/src/subagent/run.ts +++ b/src/subagent/run.ts @@ -1196,8 +1196,7 @@ async function runSubAgentInner( skillDirs, }) : undefined; - const roleBody = - modelFamilyPolicy.leafRoleBody ?? params.systemPromptRole; + const roleBody = modelFamilyPolicy.leafRoleBody ?? params.systemPromptRole; const extensions = [ ...(roleBody !== undefined ? [roleBody] : []), ...(attachedSection !== undefined ? [attachedSection] : []), From df2143e0a05b043657298951969316b6adfc28eb Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 20:02:29 -0700 Subject: [PATCH 4/6] test(subagent): pin deepseek-v4-flash coder effort at low on the wire Regression proof for the CL-10242 bake-in: a deepseek-v4-flash coder leaf resolves to reasoningEffort=low while reviewer and explorer keep their role defaults (high / medium). --- src/subagent/agent-fleet-effort.test.ts | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/subagent/agent-fleet-effort.test.ts b/src/subagent/agent-fleet-effort.test.ts index 94f5e51bc..9614ef750 100644 --- a/src/subagent/agent-fleet-effort.test.ts +++ b/src/subagent/agent-fleet-effort.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; import type { AgentProfile } from "../agent/profile-types.js"; +import type { ReasoningEffort } from "../provider/reasoning-effort.js"; import type { ProviderCatalogEntry } from "../config/index.js"; import { buildSubagentSources } from "../config/inference-sources.js"; import { clearSourceCredentials } from "../config/source-credentials.js"; @@ -167,3 +168,48 @@ describe("custom worker efforts", () => { }, ); }); + +describe("deepseek-v4-flash coder=low bake-in (CL-10242)", () => { + const dsv4: ProviderCatalogEntry = { + name: "dsv4", + baseURL: "https://dsv4.example/v1", + keyless: true, + models: ["deepseek-v4-flash"], + }; + + async function captureEffort(agent: string): Promise { + let worker: RunSubAgentParams | undefined; + const deps = createFleetDeps( + async (params) => { + worker = params; + return { report: "done" }; + }, + { settings: { providers: { dsv4 } }, catalog: [dsv4] }, + ); + deps.provider = { + providerName: dsv4.name, + baseURL: dsv4.baseURL, + model: "deepseek-v4-flash", + }; + const spawn = createSpawnAgentTool(deps); + const args: Record = { + description: "inspect", + prompt: "inspect", + agent, + }; + if (agent === "coder" || agent === "reviewer") { + args.success_criteria = ["done"]; + } + const result = await callFleetToolRaw(spawn, args); + expect(result.isError).not.toBe(true); + await callFleetToolRaw(fleetTools(deps).wait, { timeout_ms: 2000 }); + if (worker === undefined) throw new Error("worker did not start"); + return worker.provider.reasoningEffort as ReasoningEffort; + } + + test("coder pins low on the wire while reviewer/explorer keep their role defaults", async () => { + expect(await captureEffort("coder")).toBe("low"); + expect(await captureEffort("reviewer")).toBe("high"); + expect(await captureEffort("explorer")).toBe("medium"); + }); +}); From 6dd00abbaccf58db70afc174d43e5e68124e73e0 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 20:02:31 -0700 Subject: [PATCH 5/6] refactor(agent): remove CORBITS_EXP env hooks from family policy The winning deepseek-v4-flash (d1+slim, coder=low) config is now fully built-in; the eval-only CORBITS_EXP_DSV4_* file-path overrides and their __expResidual reader are deleted so no env-override path remains. --- src/agent/model-family-policy.ts | 34 +++++++++----------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/agent/model-family-policy.ts b/src/agent/model-family-policy.ts index 738b35fd0..39eb3cf94 100644 --- a/src/agent/model-family-policy.ts +++ b/src/agent/model-family-policy.ts @@ -215,8 +215,6 @@ const ASTRA_POLICY: Omit = { // body_coder.txt -> the coder leaf's role body (leafRoleBody) // body_reviewer.txt-> the reviewer leaf's role body (leafRoleBody) // g2_leaf.txt -> the explorer leaf's residual (promptResidual) -// A CORBITS_EXP_DSV4_* override still wins when set (eval-only; the removal -// of those hooks is CL-10242's follow-up deliverable). export const DSV4_D1_PRIMARY_RESIDUAL = ` Operating notes (DeepSeek V4 Flash): - Your context is the expensive one: workers do the reading, building, and checking. Read only what you need to route and to name files in a brief (one glob or grep, or one or two reads); do not read code to plan the fix yourself. @@ -266,18 +264,6 @@ Operating notes (DeepSeek V4 Flash): - Run the project's own test command once after your last edit, then write the report. `.trim(); -// EXPERIMENT HOOK (removed in the CL-10242 follow-up): lets the eval swap the -// baked-in deepseek-v4-flash residual/body text without rebuilding. A set env -// var wins over the built-in constant; the removal task deletes these and the -// __expResidual reader together. -import { readFileSync as __expReadFileSync } from "node:fs"; -function __expResidual(kind: string): string | undefined { - const path = process.env[`CORBITS_EXP_DSV4_${kind}`]; - if (path === undefined || path === "") return undefined; - const text = __expReadFileSync(path, "utf8").trim(); - return text.length > 0 ? text : undefined; -} - export function resolveModelFamilyPolicy(input: { providerName: string; model?: string; @@ -324,28 +310,26 @@ export function resolveModelFamilyPolicy(input: { // Winning config (c) bake-in (CL-10242): d1+slim-high bodies, // dispatch=max + coder=low. Orchestrators get the slim primary card // (primaryRole) and coder/reviewer/explorer leaves get their tuned - // role bodies plus the leaf residual. A CORBITS_EXP_DSV4_* env override - // wins when set (eval-only hook; removal is the CL-10242 follow-up). + // role bodies plus the leaf residual. const directorId = input.directorId; return { family, ...DEFAULT_POLICY, // Winning config (c): d1_primary.txt is the orchestrator's appended - // prompt residual (the PRIMARY hook), the explorer leaf carries the - // g2_leaf residual (LEAF_EXPLORER), and coder/reviewer leaves carry - // their tuned role bodies (BODY_CODER / BODY_REVIEWER). The slim - // primary card (primaryRole, ROLE hook) is NOT part of the winning - // set — it stays unset so orchestrators keep the shared Dispatch card. + // prompt residual, the explorer leaf carries the g2_leaf residual, + // and coder/reviewer leaves carry their tuned role bodies. The slim + // primary card (primaryRole) is NOT part of the winning set — it + // stays unset so orchestrators keep the shared Dispatch card. promptResidual: orchestrator - ? (__expResidual("PRIMARY") ?? DSV4_D1_PRIMARY_RESIDUAL) + ? DSV4_D1_PRIMARY_RESIDUAL : directorId === "explorer" - ? (__expResidual("LEAF_EXPLORER") ?? DSV4_G2_LEAF_RESIDUAL) + ? DSV4_G2_LEAF_RESIDUAL : undefined, leafRoleBody: !orchestrator && directorId === "coder" - ? (__expResidual(`BODY_CODER`) ?? DSV4_BODY_CODER) + ? DSV4_BODY_CODER : !orchestrator && directorId === "reviewer" - ? (__expResidual(`BODY_REVIEWER`) ?? DSV4_BODY_REVIEWER) + ? DSV4_BODY_REVIEWER : undefined, }; } From 7660c7c1537a84c182e535beb912a8f17d9929d5 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 20:25:25 -0700 Subject: [PATCH 6/6] refactor(agent): trim deepseek-v4-flash policy and fleet comments --- src/agent/model-family-policy.ts | 22 ++++------------------ src/subagent/agent-fleet.ts | 7 ++----- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/agent/model-family-policy.ts b/src/agent/model-family-policy.ts index 39eb3cf94..48dd720a3 100644 --- a/src/agent/model-family-policy.ts +++ b/src/agent/model-family-policy.ts @@ -206,15 +206,8 @@ const ASTRA_POLICY: Omit = { promptResidual: ASTRA_PROMPT_RESIDUAL, }; -// DeepSeek V4 Flash build-in (CL-10242) — the winning round-4 config (c): -// d1+slim-high bodies, dispatch=max + coder=low. Text is single-sourced here -// (policy owns prompt data, matching the muse/grok/claude/gpt precedent) from -// the handoff's winning prompt files: -// d1_primary.txt -> the orchestrator's appended prompt residual -// (DSV4_D1_PRIMARY_RESIDUAL, promptResidual seam) -// body_coder.txt -> the coder leaf's role body (leafRoleBody) -// body_reviewer.txt-> the reviewer leaf's role body (leafRoleBody) -// g2_leaf.txt -> the explorer leaf's residual (promptResidual) +// DeepSeek V4 Flash winning config (c), baked in from the handoff prompt +// files: d1_primary.txt, body_coder.txt, body_reviewer.txt, g2_leaf.txt. export const DSV4_D1_PRIMARY_RESIDUAL = ` Operating notes (DeepSeek V4 Flash): - Your context is the expensive one: workers do the reading, building, and checking. Read only what you need to route and to name files in a brief (one glob or grep, or one or two reads); do not read code to plan the fix yourself. @@ -307,19 +300,12 @@ export function resolveModelFamilyPolicy(input: { // gpt prompts are byte-identical — only astra carries the residual. return { family, ...ASTRA_POLICY }; case "deepseek-v4-flash": { - // Winning config (c) bake-in (CL-10242): d1+slim-high bodies, - // dispatch=max + coder=low. Orchestrators get the slim primary card - // (primaryRole) and coder/reviewer/explorer leaves get their tuned - // role bodies plus the leaf residual. + // Winning config (c): d1 primary residual on orchestrators, tuned role + // bodies for coder/reviewer leaves, g2 leaf residual on explorer. const directorId = input.directorId; return { family, ...DEFAULT_POLICY, - // Winning config (c): d1_primary.txt is the orchestrator's appended - // prompt residual, the explorer leaf carries the g2_leaf residual, - // and coder/reviewer leaves carry their tuned role bodies. The slim - // primary card (primaryRole) is NOT part of the winning set — it - // stays unset so orchestrators keep the shared Dispatch card. promptResidual: orchestrator ? DSV4_D1_PRIMARY_RESIDUAL : directorId === "explorer" diff --git a/src/subagent/agent-fleet.ts b/src/subagent/agent-fleet.ts index 10f309420..acc0b6550 100644 --- a/src/subagent/agent-fleet.ts +++ b/src/subagent/agent-fleet.ts @@ -1175,11 +1175,8 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { settings?.providers[provider.providerName], catalog?.find((entry) => entry.name === provider.providerName), ); - // DeepSeek V4 Flash bake-in (CL-10242): the winning round-4 config - // pinned the coder leaf to low. Dispatch (orchestrator) effort is set - // by the parent, not here. Only the coder role gets the built-in low - // default — an explicitly configured effort pin on the profile still - // wins — and all other workers keep their role-default cascade. + // DeepSeek V4 Flash pins the coder leaf to low unless the profile + // explicitly pins effort. const effortPin = resolved.effortPin ?? (!orchestrator &&