From 39710ce232f5c61a3d14bf08f3c2e770069fc7c4 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 5 Jul 2026 19:32:00 +0530 Subject: [PATCH 1/6] Add subagent thinking profile option --- docs/agent-profile-schema.md | 24 ++++++++++++++++++++- docs/chatgpt-coding-workflow.md | 2 +- docs/configuration.md | 2 +- examples/agents/claude-implementer.md | 1 + examples/agents/pi-reviewer.md | 1 + skills/subagent-delegation/SKILL.md | 9 ++++++-- src/cli.test.ts | 4 +++- src/cli.ts | 20 ++++++++++++----- src/db/migrations.ts | 5 ++++- src/db/schema.ts | 1 + src/local-agent-profiles.test.ts | 3 +++ src/local-agent-profiles.ts | 4 ++++ src/local-agent-runtime.ts | 1 + src/local-agent-store.test.ts | 2 ++ src/local-agent-store.ts | 11 +++++++++- src/local-agent-targets.test.ts | 31 +++++++++++++++++++++++++-- src/local-agent-targets.ts | 26 +++++++++++++++++++--- src/server.ts | 5 ++++- 18 files changed, 133 insertions(+), 19 deletions(-) diff --git a/docs/agent-profile-schema.md b/docs/agent-profile-schema.md index 382911b..4013cc3 100644 --- a/docs/agent-profile-schema.md +++ b/docs/agent-profile-schema.md @@ -20,6 +20,7 @@ name: reviewer description: Read-only reviewer for bugs, security risks, and missing tests. provider: codex model: gpt-5.4 +thinking: high disabled: false --- @@ -86,6 +87,26 @@ model: gpt-5.4 model: sonnet ``` +### `thinking` + +Optional provider reasoning effort, thinking level, or model variant. If omitted, +DevSpace lets the provider default apply. + +```yaml +thinking: low +thinking: high +thinking: xhigh +``` + +DevSpace passes this through to providers that expose a matching control: + +- `claude`: SDK effort with adaptive thinking. +- `pi`: `--thinking`. +- `opencode`: model variant. +- `cursor` and `copilot`: ACP thought-level config when supported. +- `codex`: currently depends on the Codex integration exposing an effort + control; otherwise the provider default applies. + ### `disabled` Optional boolean. Disabled profiles are not exposed. @@ -123,7 +144,8 @@ devspace agents show "name": "reviewer", "description": "Read-only reviewer for bugs, security risks, and missing tests.", "provider": "codex", - "model": "gpt-5.4" + "model": "gpt-5.4", + "thinking": "high" } ``` diff --git a/docs/chatgpt-coding-workflow.md b/docs/chatgpt-coding-workflow.md index e0ec816..6606262 100644 --- a/docs/chatgpt-coding-workflow.md +++ b/docs/chatgpt-coding-workflow.md @@ -94,7 +94,7 @@ It also keeps compatibility with: When Subagents are enabled, DevSpace discovers agent profiles from `~/.devspace/agents/*.md` and project `.devspace/agents/*.md`. `open_workspace` exposes a compact catalog with profile names, descriptions, -providers, and optional models so the model can choose a configured agent +providers, and optional models/thinking levels so the model can choose a configured agent without seeing provider-specific launch details. Example profiles are packaged under `examples/agents/` for users who want diff --git a/docs/configuration.md b/docs/configuration.md index 1601e96..4c02eb1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -115,7 +115,7 @@ from: - project `.devspace/agents/*.md` `open_workspace` returns a compact catalog containing profile names, -descriptions, providers, and optional models so the host model can choose an +descriptions, providers, and optional models/thinking levels so the host model can choose an agent without reading provider-specific launch details. `devspace agents ls` lists existing subagent sessions for the current workspace, scoped by the workspace environment injected into shell commands. The `subagent-delegation` diff --git a/examples/agents/claude-implementer.md b/examples/agents/claude-implementer.md index f4f2fb3..5443abf 100644 --- a/examples/agents/claude-implementer.md +++ b/examples/agents/claude-implementer.md @@ -4,6 +4,7 @@ name: claude-implementer description: Claude Code profile for larger implementation, refactor, and repair tasks. provider: claude model: sonnet +thinking: high --- You are a local Claude Code implementation worker under supervisor review. diff --git a/examples/agents/pi-reviewer.md b/examples/agents/pi-reviewer.md index 67eded0..8313bc1 100644 --- a/examples/agents/pi-reviewer.md +++ b/examples/agents/pi-reviewer.md @@ -3,6 +3,7 @@ schema: devspace-agent/v1 name: pi-reviewer description: Pi read-only profile for quick code review and targeted questions. provider: pi +thinking: medium --- You are a read-only local code reviewer. diff --git a/skills/subagent-delegation/SKILL.md b/skills/subagent-delegation/SKILL.md index ae2c072..cf573c2 100644 --- a/skills/subagent-delegation/SKILL.md +++ b/skills/subagent-delegation/SKILL.md @@ -48,13 +48,18 @@ Choose profiles from the compact subagent profile catalog returned by profile fits and delegation is still appropriate, use a built-in provider name from `open_workspace`. -Profiles may declare a model. To override the configured/default provider model -for a run, pass `--model`: +Profiles may declare a model and optional thinking level. To override the +configured/default provider model or thinking level for a run, pass `--model` +or `--thinking`: ```bash devspace agents run --model "" +devspace agents run --thinking "" ``` +Use `--thinking` only when the user asks for a specific reasoning depth or when +the task clearly needs a different effort than the configured profile default. + Good delegation targets: - `reviewer`: second opinion, bug risk, security risk, test gaps. diff --git a/src/cli.test.ts b/src/cli.test.ts index 6b636e1..97b7084 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -35,6 +35,7 @@ try { "description: Read-only reviewer.", "provider: codex", "model: gpt-5.4", + "thinking: high", "---", "", "Review only.", @@ -49,6 +50,7 @@ try { profileName: "reviewer", provider: "codex", model: "gpt-5.4", + thinking: "high", }).id, { status: "idle" }, ); @@ -78,7 +80,7 @@ try { }, }); - assert.match(output, new RegExp(`${current.id} idle reviewer codex gpt-5\\.4`)); + assert.match(output, new RegExp(`${current.id} idle reviewer codex gpt-5\\.4 thinking=high`)); assert.doesNotMatch(output, /profile reviewer/); assert.doesNotMatch(output, new RegExp(other.id)); diff --git a/src/cli.ts b/src/cli.ts index 7c0a01a..bbae1ae 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -371,16 +371,22 @@ async function runAgentsRun(args: string[]): Promise { store.update(existing.id, { status: "starting", model: parsed.model ?? existing.model, + thinking: parsed.thinking ?? existing.thinking, latestResponse: undefined, error: undefined, }); spawnAgentWorker(existing.id, promptFile); - console.log(formatAgentLine({ ...existing, status: "running", model: parsed.model ?? existing.model })); + console.log(formatAgentLine({ + ...existing, + status: "running", + model: parsed.model ?? existing.model, + thinking: parsed.thinking ?? existing.thinking, + })); return; } const profiles = await loadLocalAgentProfiles(config, workspaceRoot); - const target = resolveLocalAgentTarget(parsed.target, profiles, parsed.model); + const target = resolveLocalAgentTarget(parsed.target, profiles, parsed.model, parsed.thinking); if (!target) { throw new Error( `Unknown subagent profile, provider, or id: ${parsed.target}. Available ${formatAvailableLocalAgentTargets(profiles)}`, @@ -395,6 +401,7 @@ async function runAgentsRun(args: string[]): Promise { profileName: target.name, provider: target.provider, model: target.model, + thinking: target.thinking, }); spawnAgentWorker(record.id, promptFile); @@ -476,6 +483,7 @@ async function runLocalAgentProfile( providerSessionId: record.providerSessionId, writeMode: "allowed", model: record.model ?? profile.model, + thinking: record.thinking ?? profile.thinking, }); } @@ -493,6 +501,7 @@ async function runRawLocalAgentProvider( providerSessionId: record.providerSessionId, writeMode: "allowed", model: record.model, + thinking: record.thinking, }); } @@ -533,10 +542,11 @@ function resolveCurrentWorkspaceScope(): { workspaceId?: string; workspaceRoot: function formatAgentLine(agent: Pick< LocalAgentRecord, - "id" | "status" | "profileName" | "provider" | "model" + "id" | "status" | "profileName" | "provider" | "model" | "thinking" >): string { const model = agent.model ? ` ${agent.model}` : ""; - return `${agent.id} ${agent.status} ${agent.profileName} ${agent.provider}${model}`; + const thinking = agent.thinking ? ` thinking=${agent.thinking}` : ""; + return `${agent.id} ${agent.status} ${agent.profileName} ${agent.provider}${model}${thinking}`; } function sleep(ms: number): Promise { @@ -550,7 +560,7 @@ function printAgentsHelp(): void { "", "Usage:", " devspace agents ls", - " devspace agents run [--model ] ", + " devspace agents run [--model ] [--thinking ] ", " devspace agents show ", ].join("\n"), ); diff --git a/src/db/migrations.ts b/src/db/migrations.ts index 796f72f..2bda20c 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -152,6 +152,7 @@ function migrateLocalAgentSessions(sqlite: Database.Database): void { profile_name text not null, provider text not null, model text, + thinking text, provider_session_id text, status text not null, latest_response text, @@ -169,11 +170,13 @@ function migrateLocalAgentSessions(sqlite: Database.Database): void { create index if not exists local_agent_sessions_provider_session_id_idx on local_agent_sessions(provider_session_id); `); + + addColumnIfMissing(sqlite, "local_agent_sessions", "thinking", "text"); } function addColumnIfMissing( sqlite: Database.Database, - table: "workspace_sessions", + table: "workspace_sessions" | "local_agent_sessions", column: string, definition: string, ): void { diff --git a/src/db/schema.ts b/src/db/schema.ts index 6ade545..01d13fa 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -82,6 +82,7 @@ export const localAgentSessions = sqliteTable( profileName: text("profile_name").notNull(), provider: text("provider").notNull(), model: text("model"), + thinking: text("thinking"), providerSessionId: text("provider_session_id"), status: text("status").notNull(), latestResponse: text("latest_response"), diff --git a/src/local-agent-profiles.test.ts b/src/local-agent-profiles.test.ts index 4bb0bea..7665b9f 100644 --- a/src/local-agent-profiles.test.ts +++ b/src/local-agent-profiles.test.ts @@ -35,6 +35,7 @@ try { 'description: "Project reviewer #1."', "provider: claude", "model: sonnet", + "thinking: high", "---", "", "Project body.", @@ -69,12 +70,14 @@ try { assert.equal(profiles[0]?.description, "Project reviewer #1."); assert.equal(profiles[0]?.provider, "claude"); assert.equal(profiles[0]?.model, "sonnet"); + assert.equal(profiles[0]?.thinking, "high"); assert.equal(profiles[0]?.body, "Project body."); assert.deepEqual(summarizeLocalAgentProfile(profiles[0]!), { name: "reviewer", description: "Project reviewer #1.", provider: "claude", model: "sonnet", + thinking: "high", }); await writeFile( diff --git a/src/local-agent-profiles.ts b/src/local-agent-profiles.ts index 3c0f712..a7fa0d8 100644 --- a/src/local-agent-profiles.ts +++ b/src/local-agent-profiles.ts @@ -20,6 +20,7 @@ export interface LocalAgentProfile { description: string; provider: LocalAgentProvider; model?: string; + thinking?: string; filePath: string; body: string; disabled: boolean; @@ -30,6 +31,7 @@ export interface LocalAgentProfileSummary { description: string; provider: LocalAgentProvider; model?: string; + thinking?: string; } interface ParsedFrontmatter { @@ -71,6 +73,7 @@ export function summarizeLocalAgentProfile( description: profile.description, provider: profile.provider, model: profile.model, + thinking: profile.thinking, }; } @@ -154,6 +157,7 @@ function profileFromFrontmatter( description, provider, model: readString(frontmatter, "model"), + thinking: readString(frontmatter, "thinking"), filePath, body, disabled: frontmatter.disabled === true, diff --git a/src/local-agent-runtime.ts b/src/local-agent-runtime.ts index 949e73f..dc35110 100644 --- a/src/local-agent-runtime.ts +++ b/src/local-agent-runtime.ts @@ -14,6 +14,7 @@ export interface LocalAgentRunInput { providerSessionId?: string; writeMode?: LocalAgentWriteMode; model?: string; + thinking?: string; } export interface LocalAgentRunResult { diff --git a/src/local-agent-store.test.ts b/src/local-agent-store.test.ts index caf3c52..b3c0729 100644 --- a/src/local-agent-store.test.ts +++ b/src/local-agent-store.test.ts @@ -16,10 +16,12 @@ try { profileName: "reviewer", provider: "codex", model: "gpt-5.4", + thinking: "high", }); assert.match(created.id, /^agt_[a-f0-9]{8}$/); assert.equal(created.status, "starting"); + assert.equal(store.get(created.id)?.thinking, "high"); assert.equal(store.get(created.id)?.profileName, "reviewer"); assert.equal(store.get(created.id.slice(0, 7))?.id, created.id); diff --git a/src/local-agent-store.ts b/src/local-agent-store.ts index a262985..a850ca9 100644 --- a/src/local-agent-store.ts +++ b/src/local-agent-store.ts @@ -12,6 +12,7 @@ export interface LocalAgentRecord { profileName: string; provider: string; model?: string; + thinking?: string; providerSessionId?: string; status: LocalAgentStatus; latestResponse?: string; @@ -26,6 +27,7 @@ export interface CreateLocalAgentRecordInput { profileName: string; provider: string; model?: string; + thinking?: string; } export interface LocalAgentListScope { @@ -40,6 +42,7 @@ interface LocalAgentRow { profile_name: string; provider: string; model: string | null; + thinking: string | null; provider_session_id: string | null; status: string; latest_response: string | null; @@ -91,6 +94,7 @@ export class LocalAgentStore { profileName: input.profileName, provider: input.provider, model: input.model, + thinking: input.thinking, status: "starting", createdAt: now, updatedAt: now, @@ -105,10 +109,11 @@ export class LocalAgentStore { profile_name, provider, model, + thinking, status, created_at, updated_at - ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .run( record.id, @@ -117,6 +122,7 @@ export class LocalAgentStore { record.profileName, record.provider, record.model ?? null, + record.thinking ?? null, record.status, record.createdAt, record.updatedAt, @@ -164,6 +170,7 @@ export class LocalAgentStore { profile_name = ?, provider = ?, model = ?, + thinking = ?, provider_session_id = ?, status = ?, latest_response = ?, @@ -177,6 +184,7 @@ export class LocalAgentStore { updated.profileName, updated.provider, updated.model ?? null, + updated.thinking ?? null, updated.providerSessionId ?? null, updated.status, updated.latestResponse ?? null, @@ -212,6 +220,7 @@ function rowToLocalAgentRecord(row: LocalAgentRow): LocalAgentRecord { profileName: row.profile_name, provider: row.provider, model: row.model ?? undefined, + thinking: row.thinking ?? undefined, providerSessionId: row.provider_session_id ?? undefined, status: readStatus(row.status), latestResponse: row.latest_response ?? undefined, diff --git a/src/local-agent-targets.test.ts b/src/local-agent-targets.test.ts index 7dac5cd..3f1ae08 100644 --- a/src/local-agent-targets.test.ts +++ b/src/local-agent-targets.test.ts @@ -12,6 +12,7 @@ const profiles: LocalAgentProfile[] = [ description: "Review changes.", provider: "codex", model: "gpt-5-codex", + thinking: "high", filePath: "/workspace/.devspace/agents/reviewer.md", body: "Review carefully.", disabled: false, @@ -31,18 +32,35 @@ assert.deepEqual(parseLocalAgentRunArgs(["codex", "hello", "world"]), { target: "codex", prompt: "hello world", model: undefined, + thinking: undefined, }); assert.deepEqual(parseLocalAgentRunArgs(["codex", "--model", "gpt-5.1", "hello"]), { target: "codex", prompt: "hello", model: "gpt-5.1", + thinking: undefined, }); assert.deepEqual(parseLocalAgentRunArgs(["codex", "--model=gpt-5.1", "hello"]), { target: "codex", prompt: "hello", model: "gpt-5.1", + thinking: undefined, +}); + +assert.deepEqual(parseLocalAgentRunArgs(["codex", "--thinking", "high", "hello"]), { + target: "codex", + prompt: "hello", + model: undefined, + thinking: "high", +}); + +assert.deepEqual(parseLocalAgentRunArgs(["codex", "--thinking=high", "hello"]), { + target: "codex", + prompt: "hello", + model: undefined, + thinking: "high", }); assert.throws( @@ -50,18 +68,25 @@ assert.throws( /Missing value for --model/, ); +assert.throws( + () => parseLocalAgentRunArgs(["codex", "--thinking"]), + /Missing value for --thinking/, +); + { const target = resolveLocalAgentTarget("reviewer", profiles); assert.equal(target?.kind, "profile"); assert.equal(target?.name, "reviewer"); assert.equal(target?.provider, "codex"); assert.equal(target?.model, "gpt-5-codex"); + assert.equal(target?.thinking, "high"); } { - const target = resolveLocalAgentTarget("reviewer", profiles, "gpt-5.2"); + const target = resolveLocalAgentTarget("reviewer", profiles, "gpt-5.2", "xhigh"); assert.equal(target?.kind, "profile"); assert.equal(target?.model, "gpt-5.2"); + assert.equal(target?.thinking, "xhigh"); } { @@ -70,12 +95,14 @@ assert.throws( assert.equal(target?.name, "opencode"); assert.equal(target?.provider, "opencode"); assert.equal(target?.model, undefined); + assert.equal(target?.thinking, undefined); } { - const target = resolveLocalAgentTarget("opencode", profiles, "kimi-k2"); + const target = resolveLocalAgentTarget("opencode", profiles, "kimi-k2", "deep"); assert.equal(target?.kind, "provider"); assert.equal(target?.model, "kimi-k2"); + assert.equal(target?.thinking, "deep"); } { diff --git a/src/local-agent-targets.ts b/src/local-agent-targets.ts index a892d59..917e280 100644 --- a/src/local-agent-targets.ts +++ b/src/local-agent-targets.ts @@ -9,6 +9,7 @@ export interface ParsedLocalAgentRunArgs { target: string; prompt: string; model?: string; + thinking?: string; } export type LocalAgentTarget = @@ -17,6 +18,7 @@ export type LocalAgentTarget = name: string; provider: LocalAgentProvider; model?: string; + thinking?: string; profile: LocalAgentProfile; } | { @@ -24,15 +26,17 @@ export type LocalAgentTarget = name: LocalAgentProvider; provider: LocalAgentProvider; model?: string; + thinking?: string; }; export function parseLocalAgentRunArgs(args: string[]): ParsedLocalAgentRunArgs { const [target, ...rest] = args; if (!target) { - throw new Error('Usage: devspace agents run [--model ] ""'); + throw new Error('Usage: devspace agents run [--model ] [--thinking ] ""'); } let model: string | undefined; + let thinking: string | undefined; const promptParts: string[] = []; for (let index = 0; index < rest.length; index += 1) { const part = rest[index]; @@ -49,21 +53,35 @@ export function parseLocalAgentRunArgs(args: string[]): ParsedLocalAgentRunArgs model = value; continue; } + if (part === "--thinking") { + const value = rest[index + 1]?.trim(); + if (!value) throw new Error("Missing value for --thinking."); + thinking = value; + index += 1; + continue; + } + if (part?.startsWith("--thinking=")) { + const value = part.slice("--thinking=".length).trim(); + if (!value) throw new Error("Missing value for --thinking."); + thinking = value; + continue; + } promptParts.push(part ?? ""); } const prompt = promptParts.join(" ").trim(); if (!prompt) { - throw new Error('Usage: devspace agents run [--model ] ""'); + throw new Error('Usage: devspace agents run [--model ] [--thinking ] ""'); } - return { target, prompt, model }; + return { target, prompt, model, thinking }; } export function resolveLocalAgentTarget( target: string, profiles: LocalAgentProfile[], modelOverride?: string, + thinkingOverride?: string, ): LocalAgentTarget | undefined { const profile = profiles.find((candidate) => candidate.name === target); if (profile) { @@ -72,6 +90,7 @@ export function resolveLocalAgentTarget( name: profile.name, provider: profile.provider, model: modelOverride ?? profile.model, + thinking: thinkingOverride ?? profile.thinking, profile, }; } @@ -82,6 +101,7 @@ export function resolveLocalAgentTarget( name: target, provider: target, model: modelOverride, + thinking: thinkingOverride, }; } diff --git a/src/server.ts b/src/server.ts index 785229e..c72e143 100644 --- a/src/server.ts +++ b/src/server.ts @@ -196,14 +196,16 @@ function formatVisibleAgent(agent: { name: string; provider: string; model?: string; + thinking?: string; providerAvailable?: boolean; providerUnavailableReason?: string; }): string { const model = agent.model ? `, model ${agent.model}` : ""; + const thinking = agent.thinking ? `, thinking ${agent.thinking}` : ""; const availability = agent.providerAvailable === false ? `, unavailable: ${agent.providerUnavailableReason ?? "provider unavailable"}` : ""; - return `${agent.name} (${agent.provider}${model}${availability})`; + return `${agent.name} (${agent.provider}${model}${thinking}${availability})`; } function formatUnavailableAgentProvider(provider: LocalAgentProviderAvailability): string { @@ -237,6 +239,7 @@ const workspaceLocalAgentOutputSchema = z.object({ description: z.string(), provider: z.string(), model: z.string().optional(), + thinking: z.string().optional(), providerAvailable: z.boolean().optional(), providerUnavailableReason: z.string().optional(), }); From a8dfc2fb34a4837c37aa4002f8f35a358d2d79c6 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 5 Jul 2026 19:32:04 +0530 Subject: [PATCH 2/6] Pass thinking to local agent providers --- src/local-agent-adapters.test.ts | 64 ++++++++++++++++++++++++++++++++ src/local-agent-adapters.ts | 56 ++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/src/local-agent-adapters.test.ts b/src/local-agent-adapters.test.ts index 8dcea38..4f41c50 100644 --- a/src/local-agent-adapters.test.ts +++ b/src/local-agent-adapters.test.ts @@ -8,6 +8,7 @@ import { extractPiProviderError, extractPiStreamingText, piCommandEnvironment, + resolveAcpThinkingConfigUpdate, } from "./local-agent-adapters.js"; import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js"; import type { LocalAgentProvider } from "./local-agent-profiles.js"; @@ -27,6 +28,69 @@ for (const provider of providers) { assert.equal(typeof adapter.run, "function"); } +assert.deepEqual( + resolveAcpThinkingConfigUpdate({ + sessionId: "session_1", + newSessionResponse: { + configOptions: [ + { + type: "select", + id: "effort", + category: "thought_level", + options: [ + { value: "low", name: "Low" }, + { value: "high", name: "High" }, + ], + }, + ], + }, + }, "high", "cursor"), + { sessionId: "session_1", configId: "effort", value: "high" }, +); + +assert.deepEqual( + resolveAcpThinkingConfigUpdate({ + sessionId: "session_2", + newSessionResponse: { + configOptions: [ + { + type: "select", + id: "thoughts", + category: "thought_level", + options: [ + { + group: "reasoning", + name: "Reasoning", + options: [ + { value: "medium", name: "Medium" }, + { value: "xhigh", name: "X High" }, + ], + }, + ], + }, + ], + }, + }, "xhigh", "copilot"), + { sessionId: "session_2", configId: "thoughts", value: "xhigh" }, +); + +assert.throws( + () => resolveAcpThinkingConfigUpdate({ + sessionId: "session_3", + newSessionResponse: { + configOptions: [ + { + type: "select", + id: "thoughts", + category: "thought_level", + options: [{ value: "low", name: "Low" }], + }, + ], + }, + }, "max", "cursor"), + /Available values: low/, +); + { const env = claudeCommandEnvironment({ CLAUDECODE: "1", diff --git a/src/local-agent-adapters.ts b/src/local-agent-adapters.ts index 267e7f8..646ac78 100644 --- a/src/local-agent-adapters.ts +++ b/src/local-agent-adapters.ts @@ -1,6 +1,7 @@ import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process"; import { resolve } from "node:path"; import { Readable, Writable } from "node:stream"; +import type { EffortLevel } from "@anthropic-ai/claude-agent-sdk"; import type { LocalAgentProvider } from "./local-agent-profiles.js"; import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js"; import { @@ -63,6 +64,7 @@ class ClaudeLocalAgentAdapter implements LocalAgentAdapter { options: { cwd: input.workspace, model: input.model, + ...(input.thinking ? { thinking: { type: "adaptive" } as const, effort: input.thinking as EffortLevel } : {}), resume: input.providerSessionId, permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true, @@ -203,6 +205,10 @@ class AcpLocalAgentAdapter implements LocalAgentAdapter { const session = await context.buildSession(input.workspace).start(); providerSessionId = session.sessionId; try { + if (input.thinking) { + const config = resolveAcpThinkingConfigUpdate(session, input.thinking, this.provider); + await context.request(methods.agent.session.setConfigOption, config); + } const prompt = session.prompt(input.prompt); const textParts: string[] = []; for (;;) { @@ -235,6 +241,54 @@ class AcpLocalAgentAdapter implements LocalAgentAdapter { } } +export function resolveAcpThinkingConfigUpdate( + session: unknown, + thinking: string, + provider: string, +): { sessionId: string; configId: string; value: string } { + const record = asRecord(session); + if (!record) throw new Error(`${provider} ACP session did not return session metadata.`); + const sessionId = typeof record?.sessionId === "string" ? record.sessionId : undefined; + if (!sessionId) throw new Error(`${provider} ACP session did not return a session id.`); + + const response = asRecord(record.newSessionResponse); + const configOptions = response ? readArray(response, "configOptions") ?? [] : []; + const thinkingConfig = configOptions + .map(asRecord) + .find((option) => option?.type === "select" && option.category === "thought_level"); + if (!thinkingConfig) { + throw new Error(`${provider} ACP server does not expose a thinking option.`); + } + + const configId = directString(thinkingConfig.id); + if (!configId) throw new Error(`${provider} ACP thinking option is missing an id.`); + + const available = flattenAcpSelectValues(thinkingConfig); + if (!available.includes(thinking)) { + const suffix = available.length > 0 ? ` Available values: ${available.join(", ")}.` : ""; + throw new Error(`${provider} ACP thinking option does not support '${thinking}'.${suffix}`); + } + + return { sessionId, configId, value: thinking }; +} + +function flattenAcpSelectValues(option: Record): string[] { + const values: string[] = []; + for (const item of readArray(option, "options") ?? []) { + const record = asRecord(item); + const value = directString(record?.value); + if (value) { + values.push(value); + continue; + } + for (const nested of readArray(record, "options") ?? []) { + const nestedValue = directString(asRecord(nested)?.value); + if (nestedValue) values.push(nestedValue); + } + } + return values; +} + function selectAcpAllowPermissionOption(options: Array<{ optionId: string; kind: string }>): { optionId: string } | undefined { return ( options.find((option) => option.kind === "allow_once") ?? @@ -248,6 +302,7 @@ class PiRpcLocalAgentAdapter implements LocalAgentAdapter { async run(input: LocalAgentRunInput): Promise { const args = ["--mode", "rpc"]; if (input.model) args.push("--model", input.model); + if (input.thinking) args.push("--thinking", input.thinking); if (input.providerSessionId) args.push("--session", input.providerSessionId); const child = spawn(process.env.PI_COMMAND ?? "pi", args, { cwd: input.workspace, @@ -435,6 +490,7 @@ async function promptOpencodeSession( prompt: { parts: [{ type: "text", text: input.prompt }] }, parts: [{ type: "text", text: input.prompt }], ...(input.model ? { model: parseOpencodeModel(input.model) } : {}), + ...(input.thinking ? { variant: input.thinking } : {}), }; return session.prompt(promptInput, { throwOnError: true }); } From e93db12ab014af4b9454a412adaba4a6e4a1ef59 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 5 Jul 2026 19:33:19 +0530 Subject: [PATCH 3/6] Map thinking to Codex reasoning effort --- src/local-agent-runtime.test.ts | 4 ++++ src/local-agent-runtime.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/local-agent-runtime.test.ts b/src/local-agent-runtime.test.ts index c56bb46..1d45d16 100644 --- a/src/local-agent-runtime.test.ts +++ b/src/local-agent-runtime.test.ts @@ -55,6 +55,7 @@ assert.deepEqual(codex.started[0], { sandboxMode: "read-only", approvalPolicy: "never", model: undefined, + modelReasoningEffort: undefined, }); await runtime.run({ @@ -62,6 +63,7 @@ await runtime.run({ workspace: "/tmp/project", writeMode: "allowed", model: "gpt-5.4", + thinking: "high", }); assert.deepEqual(codex.started[1], { @@ -69,6 +71,7 @@ assert.deepEqual(codex.started[1], { sandboxMode: "workspace-write", approvalPolicy: "never", model: "gpt-5.4", + modelReasoningEffort: "high", }); const resumed = await runtime.run({ @@ -88,6 +91,7 @@ assert.deepEqual(codex.resumed, [ sandboxMode: "danger-full-access", approvalPolicy: "never", model: undefined, + modelReasoningEffort: undefined, }, }, ]); diff --git a/src/local-agent-runtime.ts b/src/local-agent-runtime.ts index dc35110..54130c2 100644 --- a/src/local-agent-runtime.ts +++ b/src/local-agent-runtime.ts @@ -1,6 +1,7 @@ import type { Codex, CodexOptions, + ModelReasoningEffort, RunResult, SandboxMode, ThreadOptions, @@ -59,6 +60,7 @@ function threadOptionsFor(input: LocalAgentRunInput): ThreadOptions { sandboxMode: sandboxModeFor(input.writeMode), approvalPolicy: "never", model: input.model, + modelReasoningEffort: input.thinking as ModelReasoningEffort | undefined, }; } From 0ea953eb7b0d657de45895a0090503f4d695162d Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 5 Jul 2026 19:35:05 +0530 Subject: [PATCH 4/6] Clarify Codex thinking support --- docs/agent-profile-schema.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/agent-profile-schema.md b/docs/agent-profile-schema.md index 4013cc3..0aa2d7a 100644 --- a/docs/agent-profile-schema.md +++ b/docs/agent-profile-schema.md @@ -101,11 +101,10 @@ thinking: xhigh DevSpace passes this through to providers that expose a matching control: - `claude`: SDK effort with adaptive thinking. +- `codex`: SDK model reasoning effort. - `pi`: `--thinking`. - `opencode`: model variant. - `cursor` and `copilot`: ACP thought-level config when supported. -- `codex`: currently depends on the Codex integration exposing an effort - control; otherwise the provider default applies. ### `disabled` From 74b8c228191f983152c400843038c1eec5dd6682 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 5 Jul 2026 19:43:49 +0530 Subject: [PATCH 5/6] Support ACP subagent model selection --- src/local-agent-adapters.test.ts | 64 ++++++++++++++++++++++++++++++++ src/local-agent-adapters.ts | 58 +++++++++++++++++++++++------ 2 files changed, 110 insertions(+), 12 deletions(-) diff --git a/src/local-agent-adapters.test.ts b/src/local-agent-adapters.test.ts index 4f41c50..4109e84 100644 --- a/src/local-agent-adapters.test.ts +++ b/src/local-agent-adapters.test.ts @@ -8,6 +8,7 @@ import { extractPiProviderError, extractPiStreamingText, piCommandEnvironment, + resolveAcpModelConfigUpdate, resolveAcpThinkingConfigUpdate, } from "./local-agent-adapters.js"; import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js"; @@ -28,6 +29,69 @@ for (const provider of providers) { assert.equal(typeof adapter.run, "function"); } +assert.deepEqual( + resolveAcpModelConfigUpdate({ + sessionId: "session_model_1", + newSessionResponse: { + configOptions: [ + { + type: "select", + id: "model", + category: "model", + options: [ + { value: "claude-sonnet-4.5", name: "Sonnet" }, + { value: "gpt-5.4", name: "GPT 5.4" }, + ], + }, + ], + }, + }, "gpt-5.4", "cursor"), + { sessionId: "session_model_1", configId: "model", value: "gpt-5.4" }, +); + +assert.deepEqual( + resolveAcpModelConfigUpdate({ + sessionId: "session_model_2", + newSessionResponse: { + configOptions: [ + { + type: "select", + id: "model_config", + category: "model", + options: [ + { + group: "claude", + name: "Claude", + options: [ + { value: "claude-sonnet-4.5", name: "Sonnet" }, + { value: "claude-opus-4.5", name: "Opus" }, + ], + }, + ], + }, + ], + }, + }, "claude-opus-4.5", "copilot"), + { sessionId: "session_model_2", configId: "model_config", value: "claude-opus-4.5" }, +); + +assert.throws( + () => resolveAcpModelConfigUpdate({ + sessionId: "session_model_3", + newSessionResponse: { + configOptions: [ + { + type: "select", + id: "model", + category: "model", + options: [{ value: "gpt-5.4", name: "GPT 5.4" }], + }, + ], + }, + }, "unknown-model", "cursor"), + /Available values: gpt-5\.4/, +); + assert.deepEqual( resolveAcpThinkingConfigUpdate({ sessionId: "session_1", diff --git a/src/local-agent-adapters.ts b/src/local-agent-adapters.ts index 646ac78..457b8e0 100644 --- a/src/local-agent-adapters.ts +++ b/src/local-agent-adapters.ts @@ -205,6 +205,10 @@ class AcpLocalAgentAdapter implements LocalAgentAdapter { const session = await context.buildSession(input.workspace).start(); providerSessionId = session.sessionId; try { + if (input.model) { + const config = resolveAcpModelConfigUpdate(session, input.model, this.provider); + await context.request(methods.agent.session.setConfigOption, config); + } if (input.thinking) { const config = resolveAcpThinkingConfigUpdate(session, input.thinking, this.provider); await context.request(methods.agent.session.setConfigOption, config); @@ -241,35 +245,65 @@ class AcpLocalAgentAdapter implements LocalAgentAdapter { } } +export function resolveAcpModelConfigUpdate( + session: unknown, + model: string, + provider: string, +): { sessionId: string; configId: string; value: string } { + return resolveAcpSelectConfigUpdate(session, { + category: "model", + label: "model", + provider, + value: model, + }); +} + export function resolveAcpThinkingConfigUpdate( session: unknown, thinking: string, provider: string, +): { sessionId: string; configId: string; value: string } { + return resolveAcpSelectConfigUpdate(session, { + category: "thought_level", + label: "thinking option", + provider, + value: thinking, + }); +} + +function resolveAcpSelectConfigUpdate( + session: unknown, + options: { + category: string; + label: string; + provider: string; + value: string; + }, ): { sessionId: string; configId: string; value: string } { const record = asRecord(session); - if (!record) throw new Error(`${provider} ACP session did not return session metadata.`); + if (!record) throw new Error(`${options.provider} ACP session did not return session metadata.`); const sessionId = typeof record?.sessionId === "string" ? record.sessionId : undefined; - if (!sessionId) throw new Error(`${provider} ACP session did not return a session id.`); + if (!sessionId) throw new Error(`${options.provider} ACP session did not return a session id.`); const response = asRecord(record.newSessionResponse); const configOptions = response ? readArray(response, "configOptions") ?? [] : []; - const thinkingConfig = configOptions + const config = configOptions .map(asRecord) - .find((option) => option?.type === "select" && option.category === "thought_level"); - if (!thinkingConfig) { - throw new Error(`${provider} ACP server does not expose a thinking option.`); + .find((option) => option?.type === "select" && option.category === options.category); + if (!config) { + throw new Error(`${options.provider} ACP server does not expose a ${options.label}.`); } - const configId = directString(thinkingConfig.id); - if (!configId) throw new Error(`${provider} ACP thinking option is missing an id.`); + const configId = directString(config.id); + if (!configId) throw new Error(`${options.provider} ACP ${options.label} is missing an id.`); - const available = flattenAcpSelectValues(thinkingConfig); - if (!available.includes(thinking)) { + const available = flattenAcpSelectValues(config); + if (!available.includes(options.value)) { const suffix = available.length > 0 ? ` Available values: ${available.join(", ")}.` : ""; - throw new Error(`${provider} ACP thinking option does not support '${thinking}'.${suffix}`); + throw new Error(`${options.provider} ACP ${options.label} does not support '${options.value}'.${suffix}`); } - return { sessionId, configId, value: thinking }; + return { sessionId, configId, value: options.value }; } function flattenAcpSelectValues(option: Record): string[] { From b34f5406e023b34ad1ed3076ee7b3349f7408412 Mon Sep 17 00:00:00 2001 From: Waishnav Date: Sun, 5 Jul 2026 19:55:18 +0530 Subject: [PATCH 6/6] Address subagent thinking review feedback --- docs/agent-profile-schema.md | 3 ++- skills/subagent-delegation/SKILL.md | 3 +++ src/local-agent-adapters.test.ts | 36 +++++++++++++++++++++++++++++ src/local-agent-store.test.ts | 3 +++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/agent-profile-schema.md b/docs/agent-profile-schema.md index 0aa2d7a..0dc3db9 100644 --- a/docs/agent-profile-schema.md +++ b/docs/agent-profile-schema.md @@ -90,7 +90,8 @@ model: sonnet ### `thinking` Optional provider reasoning effort, thinking level, or model variant. If omitted, -DevSpace lets the provider default apply. +DevSpace lets the provider default apply. Values are provider-specific +passthrough strings; DevSpace does not translate names between harnesses. ```yaml thinking: low diff --git a/skills/subagent-delegation/SKILL.md b/skills/subagent-delegation/SKILL.md index cf573c2..fb269df 100644 --- a/skills/subagent-delegation/SKILL.md +++ b/skills/subagent-delegation/SKILL.md @@ -59,6 +59,9 @@ devspace agents run --thinking "" Use `--thinking` only when the user asks for a specific reasoning depth or when the task clearly needs a different effort than the configured profile default. +Thinking values are provider-specific passthrough values. Use names supported by +the selected local agent harness; DevSpace does not translate values between +providers. Good delegation targets: diff --git a/src/local-agent-adapters.test.ts b/src/local-agent-adapters.test.ts index 4109e84..0e4cbf0 100644 --- a/src/local-agent-adapters.test.ts +++ b/src/local-agent-adapters.test.ts @@ -92,6 +92,24 @@ assert.throws( /Available values: gpt-5\.4/, ); +assert.throws( + () => resolveAcpModelConfigUpdate(undefined, "gpt-5.4", "cursor"), + /session metadata/, +); + +assert.throws( + () => resolveAcpModelConfigUpdate({ newSessionResponse: { configOptions: [] } }, "gpt-5.4", "cursor"), + /session id/, +); + +assert.throws( + () => resolveAcpModelConfigUpdate({ + sessionId: "session_model_4", + newSessionResponse: { configOptions: [] }, + }, "gpt-5.4", "cursor"), + /does not expose a model/, +); + assert.deepEqual( resolveAcpThinkingConfigUpdate({ sessionId: "session_1", @@ -155,6 +173,24 @@ assert.throws( /Available values: low/, ); +assert.throws( + () => resolveAcpThinkingConfigUpdate(undefined, "high", "copilot"), + /session metadata/, +); + +assert.throws( + () => resolveAcpThinkingConfigUpdate({ newSessionResponse: { configOptions: [] } }, "high", "copilot"), + /session id/, +); + +assert.throws( + () => resolveAcpThinkingConfigUpdate({ + sessionId: "session_4", + newSessionResponse: { configOptions: [] }, + }, "high", "copilot"), + /does not expose a thinking option/, +); + { const env = claudeCommandEnvironment({ CLAUDECODE: "1", diff --git a/src/local-agent-store.test.ts b/src/local-agent-store.test.ts index b3c0729..cf7265a 100644 --- a/src/local-agent-store.test.ts +++ b/src/local-agent-store.test.ts @@ -29,10 +29,13 @@ try { status: "idle", latestResponse: "done", providerSessionId: "thread_123", + thinking: "medium", }); assert.equal(updated.status, "idle"); + assert.equal(updated.thinking, "medium"); assert.equal(store.get("thread_123")?.id, created.id); + assert.equal(store.get(created.id)?.thinking, "medium"); assert.equal(store.update(created.id, { latestResponse: undefined }).latestResponse, undefined); assert.deepEqual( store.list({ workspaceRoot: join(root, "project") }).map((agent) => agent.latestResponse),