From 922227ab48c35f29944b53fd139b81d6d79850b5 Mon Sep 17 00:00:00 2001 From: sasicodes <29498872+sasicodes@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:35:55 +0530 Subject: [PATCH 1/8] feat: per-task model and effort selection for workflows --- packages/desktop/src/main/chat.ts | 23 ++++++++- packages/desktop/src/main/models.ts | 22 ++++++-- .../src/main/providers/tools/subagents.ts | 34 ++++++++----- .../desktop/src/main/subagents/runtime.ts | 50 +++++++++++------- packages/desktop/src/main/subagents/types.ts | 13 ++++- .../src/main/subagents/utils/catalog.ts | 14 +++++ .../desktop/src/main/subagents/utils/input.ts | 18 +++---- packages/desktop/src/main/types.ts | 2 +- .../tests/units/subagents-runtime.test.ts | 14 ++--- .../tests/units/tools/subagents.test.ts | 51 +++++++++++-------- .../units/tools/workflow-catalog.test.ts | 43 ++++++++++++++++ 11 files changed, 206 insertions(+), 78 deletions(-) create mode 100644 packages/desktop/src/main/subagents/utils/catalog.ts create mode 100644 packages/desktop/tests/units/tools/workflow-catalog.test.ts diff --git a/packages/desktop/src/main/chat.ts b/packages/desktop/src/main/chat.ts index d7e4f504..653c9bc5 100644 --- a/packages/desktop/src/main/chat.ts +++ b/packages/desktop/src/main/chat.ts @@ -64,8 +64,10 @@ import { updateSessionTitle, upsertSessionOnStart } from '@main/sessions'; +import { modelScore } from '@main/models'; import { readStartState, type StartState, updateStartState } from '@main/storage'; import { SubagentNameAllocator } from '@main/subagents/allocator'; +import type { WorkflowModelOption } from '@main/subagents/types'; import { type AgentTab, type AgentTabStatus, @@ -1998,11 +2000,12 @@ export class ChatService { const base = { cwd: () => cwd, authStorage: this.authStorage, + resolveModel: (key: string) => this.findModelByKey(key) ?? null, nameAllocator: () => allocator, modelRegistry: this.modelRegistry, model: () => this.pickModel() ?? null, - settingsManager: this.settingsManager, - thinkingLevel: () => this.selectedThinkingLevel + availableModels: () => this.workflowModels(), + settingsManager: this.settingsManager }; const subagentSessionTools = (): ReturnType => createStartCustomTools({ ...base, includeSubagents: false, customTools: subagentSessionTools }); @@ -2139,6 +2142,22 @@ export class ChatService { return getVisibleModels(this.modelRegistry.getAvailable()); } + private workflowModels(): WorkflowModelOption[] { + return this.modelRegistry.getAvailable().flatMap((model) => { + const score = modelScore(model.provider, model.id); + if (!score) return []; + return [ + { + score, + key: modelKey(model), + name: model.name, + provider: model.provider, + effortLevels: getSupportedEffortLevels(model) + } + ]; + }); + } + private isThinkingLevel(level: string): level is EffortLevel { return effortLevels.includes(level as EffortLevel); } diff --git a/packages/desktop/src/main/models.ts b/packages/desktop/src/main/models.ts index 00d76cc1..2e044b28 100644 --- a/packages/desktop/src/main/models.ts +++ b/packages/desktop/src/main/models.ts @@ -1,18 +1,30 @@ import type { ProviderKey } from '@main/types'; +export interface ModelScore { + taste: number; + intelligence: number; + affordability: number; +} + export interface AllowedModel { id: string; + score: ModelScore; provider: ProviderKey; } export const models: readonly AllowedModel[] = [ - { provider: 'openai', id: 'gpt-5.5' }, - { provider: 'openai', id: 'gpt-5.4' }, - { provider: 'anthropic', id: 'claude-fable-5' }, - { provider: 'anthropic', id: 'claude-opus-4-8' }, - { provider: 'anthropic', id: 'claude-sonnet-5' } + { provider: 'openai', id: 'gpt-5.5', score: { affordability: 9, intelligence: 8, taste: 5 } }, + { provider: 'openai', id: 'gpt-5.4', score: { affordability: 8, intelligence: 6, taste: 4 } }, + { provider: 'anthropic', id: 'claude-fable-5', score: { affordability: 2, intelligence: 9, taste: 9 } }, + { provider: 'anthropic', id: 'claude-opus-4-8', score: { affordability: 4, intelligence: 7, taste: 8 } }, + { provider: 'anthropic', id: 'claude-sonnet-5', score: { affordability: 5, intelligence: 5, taste: 7 } } ] as const; +const scoresByKey = new Map(models.map((model) => [`${model.provider}:${model.id}`, model.score])); + +export const modelScore = (provider: string, id: string): ModelScore | undefined => + scoresByKey.get(`${provider}:${id}`); + const idsByProvider = (provider: ProviderKey) => models.filter((model) => model.provider === provider).map((model) => model.id); diff --git a/packages/desktop/src/main/providers/tools/subagents.ts b/packages/desktop/src/main/providers/tools/subagents.ts index d6e99beb..d1f7570d 100644 --- a/packages/desktop/src/main/providers/tools/subagents.ts +++ b/packages/desktop/src/main/providers/tools/subagents.ts @@ -8,8 +8,10 @@ import { import { toolResult } from '@main/providers/tools/result'; import type { SubagentNameAllocator } from '@main/subagents/allocator'; import { runSubagents } from '@main/subagents/runtime'; +import type { WorkflowModelOption } from '@main/subagents/types'; +import { workflowToolDescription } from '@main/subagents/utils/catalog'; import { maxSubagentTasks, normalizeSubagentTasks } from '@main/subagents/utils/input'; -import type { EffortLevel } from '@main/types'; +import { effortLevels } from '@main/types'; const spawnToolParameters = { properties: { @@ -20,12 +22,21 @@ const spawnToolParameters = { description: 'Focused tasks to run in parallel sub-agents.', items: { type: 'object', - required: ['prompt'], + required: ['prompt', 'model', 'effort'], additionalProperties: false, properties: { prompt: { type: 'string', description: 'Focused task for one sub-agent.' + }, + model: { + type: 'string', + description: 'Model key for this task, chosen from the models listed in the tool description.' + }, + effort: { + type: 'string', + enum: effortLevels, + description: 'Reasoning effort for this task.' } } } @@ -36,25 +47,27 @@ const spawnToolParameters = { additionalProperties: false } as const; +type ResolvedModel = ModelRegistry['getAvailable'] extends () => Array ? ModelItem : never; + interface CreateSubagentToolsOptions { cwd: () => string; authStorage: AuthStorage; modelRegistry: ModelRegistry; - thinkingLevel: () => EffortLevel; settingsManager: SettingsManager; customTools: () => ToolDefinition[]; nameAllocator: () => SubagentNameAllocator; - model: () => ModelRegistry['getAvailable'] extends () => Array ? ModelItem | null : never; + availableModels: () => WorkflowModelOption[]; + resolveModel: (key: string) => ResolvedModel | null; } export const createSubagentTools = ({ cwd, - model, authStorage, customTools, + resolveModel, modelRegistry, nameAllocator, - thinkingLevel, + availableModels, settingsManager }: CreateSubagentToolsOptions) => [ defineTool({ @@ -62,25 +75,22 @@ export const createSubagentTools = ({ name: 'run_workflow', executionMode: 'sequential', parameters: spawnToolParameters, - description: 'Run a workflow of focused sub-agents in parallel.', + description: workflowToolDescription(availableModels()), promptSnippet: 'Use for independent research, review, or mapping work.', prepareArguments: (args) => ({ tasks: normalizeSubagentTasks(args) }), async execute(_toolCallId, { tasks }, signal, onUpdate) { - const selectedModel = model(); - if (!selectedModel) throw new Error('No configured model is available for sub-agents.'); - if (tasks.length === 0) throw new Error('Enter at least one sub-agent task.'); + if (tasks.length === 0) throw new Error('Each workflow task needs a prompt, a model, and an effort level.'); const result = await runSubagents({ tasks, cwd: cwd(), authStorage, customTools, + resolveModel, modelRegistry, settingsManager, - model: selectedModel, ...(signal ? { signal } : {}), nameAllocator: nameAllocator(), - thinkingLevel: thinkingLevel(), onUpdate: (snapshot) => onUpdate?.(toolResult('Sub-agents are working.', snapshot)) }); diff --git a/packages/desktop/src/main/subagents/runtime.ts b/packages/desktop/src/main/subagents/runtime.ts index d6346688..cb61c41d 100644 --- a/packages/desktop/src/main/subagents/runtime.ts +++ b/packages/desktop/src/main/subagents/runtime.ts @@ -9,25 +9,31 @@ import { type ToolDefinition } from '@earendil-works/pi-coding-agent'; import { countLabel } from '@main/details'; -import { agentEndError } from '@main/helpers'; +import { agentEndError, clampThinkingLevel } from '@main/helpers'; import { createStartResourceLoader } from '@main/prompt/loader'; import type { SubagentNameAllocator } from '@main/subagents/allocator'; import { subagentAccentColor, subagentAvatar } from '@main/subagents/avatar'; import type { SubagentRunResult, SubagentRunSnapshot, SubagentTaskInput } from '@main/subagents/types'; -import type { EffortLevel, SubagentActivity } from '@main/types'; +import type { SubagentActivity } from '@main/types'; + +type ResolvedModel = ModelRegistry['getAvailable'] extends () => Array ? ModelItem : never; interface RunSubagentsOptions { cwd: string; signal?: AbortSignal; authStorage: AuthStorage; tasks: SubagentTaskInput[]; - thinkingLevel: EffortLevel; modelRegistry: ModelRegistry; settingsManager: SettingsManager; customTools: () => ToolDefinition[]; nameAllocator: SubagentNameAllocator; onUpdate: (snapshot: SubagentRunSnapshot) => void; - model: ModelRegistry['getAvailable'] extends () => Array ? ModelItem : never; + resolveModel: (key: string) => ResolvedModel | null; +} + +interface AgentJob { + task: SubagentTaskInput; + agent: SubagentActivity; } const maxConcurrentAgents = 4; @@ -88,35 +94,41 @@ const runWithAbort = async (session: AgentSession, signal: AbortSignal | null, t export const runSubagents = async ({ cwd, - model, tasks, signal, onUpdate, authStorage, customTools, + resolveModel, modelRegistry, nameAllocator, - thinkingLevel, settingsManager }: RunSubagentsOptions): Promise => { - const agents: SubagentActivity[] = tasks.map((task, index) => { + const jobs: AgentJob[] = tasks.map((task, index) => { const name = nameAllocator.next(`${task.prompt}:${index}:${randomUUID()}`); return { - name, - id: randomUUID(), - status: 'queued', - task: task.prompt, - avatar: subagentAvatar(name), - accentColor: subagentAccentColor(name) + task, + agent: { + name, + id: randomUUID(), + status: 'queued', + task: task.prompt, + avatar: subagentAvatar(name), + accentColor: subagentAccentColor(name) + } }; }); + const agents = jobs.map((job) => job.agent); const update = () => onUpdate({ agents: agents.map((agent) => ({ ...agent })) }); update(); - const runAgent = async (agent: SubagentActivity): Promise => { + const runAgent = async ({ task, agent }: AgentJob): Promise => { let session: AgentSession | null = null; try { + const model = resolveModel(task.model); + if (!model) throw new Error(`Model ${task.model} is not available.`); + agent.status = 'running'; update(); @@ -127,11 +139,11 @@ export const runSubagents = async ({ model, authStorage, modelRegistry, - thinkingLevel, sessionManager, resourceLoader, customTools: customTools(), - settingsManager + settingsManager, + thinkingLevel: clampThinkingLevel(model, task.effort) }); session = result.session; session.setActiveToolsByName(session.getAllTools().map(({ name }) => name)); @@ -162,11 +174,11 @@ export const runSubagents = async ({ } }; - const pending = [...agents]; + const pending = [...jobs]; const worker = async () => { - for (let agent = pending.shift(); agent; agent = pending.shift()) await runAgent(agent); + for (let job = pending.shift(); job; job = pending.shift()) await runAgent(job); }; - await Promise.all(Array.from({ length: Math.min(maxConcurrentAgents, agents.length) }, worker)); + await Promise.all(Array.from({ length: Math.min(maxConcurrentAgents, jobs.length) }, worker)); return { text: `${countLabel(agents.length, 'sub-agent')} finished.\n\n${resultText(agents)}`, diff --git a/packages/desktop/src/main/subagents/types.ts b/packages/desktop/src/main/subagents/types.ts index c0065de2..950a8e05 100644 --- a/packages/desktop/src/main/subagents/types.ts +++ b/packages/desktop/src/main/subagents/types.ts @@ -1,7 +1,18 @@ -import type { SubagentActivity } from '@main/types'; +import type { ModelScore } from '@main/models'; +import type { EffortLevel, SubagentActivity } from '@main/types'; export interface SubagentTaskInput { + model: string; prompt: string; + effort: EffortLevel; +} + +export interface WorkflowModelOption { + key: string; + name: string; + score: ModelScore; + provider: string; + effortLevels: EffortLevel[]; } export interface SubagentRunSnapshot { diff --git a/packages/desktop/src/main/subagents/utils/catalog.ts b/packages/desktop/src/main/subagents/utils/catalog.ts new file mode 100644 index 00000000..81783ef2 --- /dev/null +++ b/packages/desktop/src/main/subagents/utils/catalog.ts @@ -0,0 +1,14 @@ +import type { WorkflowModelOption } from '@main/subagents/types'; + +const scoreLine = (option: WorkflowModelOption) => + `- ${option.key} (${option.name}): affordability ${option.score.affordability}, intelligence ${option.score.intelligence}, taste ${option.score.taste}; effort ${option.effortLevels.join('/')}`; + +export const workflowModelMenu = (options: WorkflowModelOption[]) => options.map(scoreLine).join('\n'); + +export const workflowToolDescription = (options: WorkflowModelOption[]) => { + if (options.length === 0) { + return 'Run a workflow of focused sub-agents in parallel. No models are configured; set up a provider before spawning sub-agents.'; + } + + return `Run a workflow of focused sub-agents in parallel. Pick a model and effort for each task from the scores below (all scores are 0-10, higher is better; affordability higher means cheaper). Match cheap simple work to a cheaper, lower-intelligence model with low effort; give design, UX, and architecture work a higher-taste model; give hard reasoning a higher-intelligence model with high or xhigh effort.\n\nModels:\n${workflowModelMenu(options)}`; +}; diff --git a/packages/desktop/src/main/subagents/utils/input.ts b/packages/desktop/src/main/subagents/utils/input.ts index 9535f273..70ae9f28 100644 --- a/packages/desktop/src/main/subagents/utils/input.ts +++ b/packages/desktop/src/main/subagents/utils/input.ts @@ -1,19 +1,13 @@ import { isRecord } from '@main/details'; import type { SubagentTaskInput } from '@main/subagents/types'; +import { effortLevels } from '@main/types'; import * as v from 'valibot'; -const taskSchema = v.pipe( - v.union([ - v.string(), - v.pipe( - v.object({ prompt: v.string() }), - v.transform((task) => task.prompt) - ) - ]), - v.trim(), - v.minLength(1), - v.transform((prompt) => ({ prompt }) satisfies SubagentTaskInput) -); +const taskSchema = v.object({ + effort: v.picklist(effortLevels), + model: v.pipe(v.string(), v.trim(), v.minLength(1)), + prompt: v.pipe(v.string(), v.trim(), v.minLength(1)) +}) satisfies v.GenericSchema; export const maxSubagentTasks = 8; diff --git a/packages/desktop/src/main/types.ts b/packages/desktop/src/main/types.ts index 99860f15..df4ba4e4 100644 --- a/packages/desktop/src/main/types.ts +++ b/packages/desktop/src/main/types.ts @@ -2,7 +2,7 @@ export type EffortLevel = 'high' | 'low' | 'xhigh' | 'medium'; export type ThinkingLevel = 'off' | 'minimal' | EffortLevel; export type ProviderKey = 'openai' | 'anthropic'; -export const effortLevels: EffortLevel[] = ['low', 'medium', 'high', 'xhigh']; +export const effortLevels = ['low', 'medium', 'high', 'xhigh'] as const satisfies readonly EffortLevel[]; export type ChatStatus = { ready: boolean; diff --git a/packages/desktop/tests/units/subagents-runtime.test.ts b/packages/desktop/tests/units/subagents-runtime.test.ts index 64ec1428..cefb172f 100644 --- a/packages/desktop/tests/units/subagents-runtime.test.ts +++ b/packages/desktop/tests/units/subagents-runtime.test.ts @@ -30,14 +30,13 @@ describe('sub-agent runtime', () => { it('keeps full sub-agent tools enabled', async () => { const snapshots: SubagentRunSnapshot[] = []; const run = runSubagents({ - model, authStorage, modelRegistry, settingsManager, + resolveModel: () => model, cwd: '/workspace/project', - tasks: [{ prompt: 'Inspect the project.' }], + tasks: [{ prompt: 'Inspect the project.', model: 'anthropic:claude-opus-4-7', effort: 'medium' }], nameAllocator: new SubagentNameAllocator(), - thinkingLevel: 'medium', customTools: () => [tool('browser_open'), tool('browser_snapshot')], onUpdate: (snapshot) => snapshots.push(snapshot) }); @@ -59,16 +58,19 @@ describe('sub-agent runtime', () => { it('runs at most four sub-agents at once', async () => { const run = runSubagents({ - model, authStorage, modelRegistry, settingsManager, onUpdate: () => {}, customTools: () => [], - thinkingLevel: 'medium', + resolveModel: () => model, cwd: '/workspace/project', nameAllocator: new SubagentNameAllocator(), - tasks: Array.from({ length: 6 }, (_, index) => ({ prompt: `Task ${index}.` })) + tasks: Array.from({ length: 6 }, (_, index) => ({ + prompt: `Task ${index}.`, + model: 'anthropic:claude-opus-4-7', + effort: 'medium' as const + })) }); await vi.waitFor(() => expect(listFakeSessions()).toHaveLength(4)); diff --git a/packages/desktop/tests/units/tools/subagents.test.ts b/packages/desktop/tests/units/tools/subagents.test.ts index c0200244..f0b8ebd1 100644 --- a/packages/desktop/tests/units/tools/subagents.test.ts +++ b/packages/desktop/tests/units/tools/subagents.test.ts @@ -22,7 +22,12 @@ describe('sub-agent tool details', () => { agents: [{ id: 'agent-1', name: 'Arul', status: 'unknown' }] } }, - args: { tasks: [{ prompt: 'Review UI.' }, { prompt: 'Review tests.' }] }, + args: { + tasks: [ + { prompt: 'Review UI.', model: 'anthropic:claude-opus-4-7', effort: 'low' }, + { prompt: 'Review tests.', model: 'anthropic:claude-opus-4-7', effort: 'low' } + ] + }, key: 'tool:subagents', toolName: 'run_workflow' }); @@ -37,7 +42,7 @@ describe('sub-agent tool details', () => { state: 'active', toolName: 'run_workflow', key: 'tool:subagents', - args: { tasks: [{ prompt: 'Review renderer activity UI.' }] }, + args: { tasks: [{ prompt: 'Review renderer activity UI.', model: 'anthropic:claude-opus-4-7', effort: 'high' }] }, result: { content: [{ text: 'Sub-agents are working.', type: 'text' }], details: { @@ -62,41 +67,47 @@ describe('sub-agent tool details', () => { expect(detail.subagents?.[0]?.task).toBe('Review renderer activity UI.'); }); - it('normalizes Claude single-task sub-agent arguments', () => { - expect(normalizeSubagentTasks({ prompt: 'Review renderer activity UI.' })).toEqual([ - { prompt: 'Review renderer activity UI.' } - ]); - expect(normalizeSubagentTasks({ tasks: { prompt: 'Review main process.' } })).toEqual([ - { prompt: 'Review main process.' } - ]); - expect(normalizeSubagentTasks({ tasks: [{ prompt: 'Review UI.' }, { prompt: 'Review tests.' }] })).toEqual([ - { prompt: 'Review UI.' }, - { prompt: 'Review tests.' } - ]); - expect(normalizeSubagentTasks(['Review UI.', 'Review tests.'])).toEqual([ - { prompt: 'Review UI.' }, - { prompt: 'Review tests.' } + it('normalizes single-task workflow arguments and requires model and effort', () => { + const single = { prompt: 'Review renderer activity UI.', model: 'anthropic:claude-opus-4-7', effort: 'low' }; + expect(normalizeSubagentTasks(single)).toEqual([single]); + expect(normalizeSubagentTasks({ tasks: single })).toEqual([single]); + expect( + normalizeSubagentTasks({ + tasks: [ + { prompt: 'Review UI.', model: 'openai:gpt-5.5', effort: 'high' }, + { prompt: 'Review tests.', model: 'anthropic:claude-sonnet-5', effort: 'low' } + ] + }) + ).toEqual([ + { prompt: 'Review UI.', model: 'openai:gpt-5.5', effort: 'high' }, + { prompt: 'Review tests.', model: 'anthropic:claude-sonnet-5', effort: 'low' } ]); - expect(normalizeSubagentTasks({ tasks: [{ prompt: ' ' }, { name: 'no prompt' }] })).toEqual([]); + expect(normalizeSubagentTasks(['Review UI.', 'Review tests.'])).toEqual([]); + expect(normalizeSubagentTasks({ tasks: [{ prompt: 'No model or effort.' }] })).toEqual([]); + expect(normalizeSubagentTasks({ tasks: [{ prompt: ' ', model: 'openai:gpt-5.5', effort: 'low' }] })).toEqual([]); expect(normalizeSubagentTasks({})).toEqual([]); const detail = toolEventDetail({ state: 'active', toolName: 'run_workflow', key: 'tool:subagents', - args: { prompt: 'Review renderer activity UI.' } + args: single }); expect(detail.title).toBe('Spawning 1 agent'); }); it('caps normalized tasks at the spawn limit', () => { - const tasks = Array.from({ length: 12 }, (_, index) => ({ prompt: `Task ${index}` })); + const tasks = Array.from({ length: 12 }, (_, index) => ({ + prompt: `Task ${index}`, + model: 'anthropic:claude-opus-4-7', + effort: 'low' + })); const normalized = normalizeSubagentTasks({ tasks }); expect(maxSubagentTasks).toBe(8); expect(normalized).toHaveLength(maxSubagentTasks); - expect(normalized.at(-1)).toEqual({ prompt: 'Task 7' }); + expect(normalized.at(-1)).toEqual({ prompt: 'Task 7', model: 'anthropic:claude-opus-4-7', effort: 'low' }); }); it('expands completed summaries', () => { diff --git a/packages/desktop/tests/units/tools/workflow-catalog.test.ts b/packages/desktop/tests/units/tools/workflow-catalog.test.ts new file mode 100644 index 00000000..916ed45c --- /dev/null +++ b/packages/desktop/tests/units/tools/workflow-catalog.test.ts @@ -0,0 +1,43 @@ +import type { WorkflowModelOption } from '@main/subagents/types'; +import { workflowModelMenu, workflowToolDescription } from '@main/subagents/utils/catalog'; +import { describe, expect, it } from 'vitest'; + +const option = (overrides: Partial): WorkflowModelOption => ({ + key: 'anthropic:claude-opus-4-8', + name: 'Claude Opus 4 8', + provider: 'anthropic', + effortLevels: ['low', 'medium', 'high'], + score: { affordability: 4, intelligence: 7, taste: 8 }, + ...overrides +}); + +describe('workflow catalog', () => { + it('renders one scored line per available model', () => { + const menu = workflowModelMenu([ + option({ key: 'openai:gpt-5.5', name: 'GPT 5.5', score: { affordability: 9, intelligence: 8, taste: 5 } }), + option({}) + ]); + + expect(menu).toBe( + [ + '- openai:gpt-5.5 (GPT 5.5): affordability 9, intelligence 8, taste 5; effort low/medium/high', + '- anthropic:claude-opus-4-8 (Claude Opus 4 8): affordability 4, intelligence 7, taste 8; effort low/medium/high' + ].join('\n') + ); + }); + + it('explains scoring and lists models when some are available', () => { + const description = workflowToolDescription([option({})]); + + expect(description).toContain('higher is better'); + expect(description).toContain('affordability higher means cheaper'); + expect(description).toContain('anthropic:claude-opus-4-8'); + }); + + it('tells the agent to set up a provider when nothing is available', () => { + const description = workflowToolDescription([]); + + expect(description).toContain('No models are configured'); + expect(description).not.toContain('affordability higher means cheaper'); + }); +}); From 6d863c93ddd439ffa83a4a16babfe8d756466bd6 Mon Sep 17 00:00:00 2001 From: sasicodes <29498872+sasicodes@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:05:03 +0530 Subject: [PATCH 2/8] feat: guide workflow model choice toward cost efficiency --- packages/desktop/src/main/subagents/utils/catalog.ts | 2 +- packages/desktop/tests/units/tools/workflow-catalog.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/main/subagents/utils/catalog.ts b/packages/desktop/src/main/subagents/utils/catalog.ts index 81783ef2..6e86a6cc 100644 --- a/packages/desktop/src/main/subagents/utils/catalog.ts +++ b/packages/desktop/src/main/subagents/utils/catalog.ts @@ -10,5 +10,5 @@ export const workflowToolDescription = (options: WorkflowModelOption[]) => { return 'Run a workflow of focused sub-agents in parallel. No models are configured; set up a provider before spawning sub-agents.'; } - return `Run a workflow of focused sub-agents in parallel. Pick a model and effort for each task from the scores below (all scores are 0-10, higher is better; affordability higher means cheaper). Match cheap simple work to a cheaper, lower-intelligence model with low effort; give design, UX, and architecture work a higher-taste model; give hard reasoning a higher-intelligence model with high or xhigh effort.\n\nModels:\n${workflowModelMenu(options)}`; + return `Run a workflow of focused sub-agents in parallel. Choose a model and effort per task from the scores below (0-10, higher is better; affordability higher means cheaper). Do not default to the highest scores, that wastes tokens. Pick the cheapest model and lowest effort that can still do the task well, and only move up when the task truly needs it: reading, lookups, and mechanical edits belong on a cheap model at low effort; design, UX, and architecture want higher taste; genuinely hard reasoning wants higher intelligence at high or xhigh effort. Match each task to the minimum capability it needs.\n\nModels:\n${workflowModelMenu(options)}`; }; diff --git a/packages/desktop/tests/units/tools/workflow-catalog.test.ts b/packages/desktop/tests/units/tools/workflow-catalog.test.ts index 916ed45c..2796e3c1 100644 --- a/packages/desktop/tests/units/tools/workflow-catalog.test.ts +++ b/packages/desktop/tests/units/tools/workflow-catalog.test.ts @@ -26,11 +26,13 @@ describe('workflow catalog', () => { ); }); - it('explains scoring and lists models when some are available', () => { + it('explains scoring, tells the agent to conserve tokens, and lists models when some are available', () => { const description = workflowToolDescription([option({})]); expect(description).toContain('higher is better'); expect(description).toContain('affordability higher means cheaper'); + expect(description).toContain('Do not default to the highest scores'); + expect(description).toContain('minimum capability it needs'); expect(description).toContain('anthropic:claude-opus-4-8'); }); From 33ebfe479ead388eb5b02dbfcf4718840433d4ea Mon Sep 17 00:00:00 2001 From: sasicodes <29498872+sasicodes@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:09:52 +0530 Subject: [PATCH 3/8] chore: length-sort workflow model members and keys --- packages/desktop/src/main/chat.ts | 6 +++--- packages/desktop/src/main/models.ts | 10 +++++----- packages/desktop/src/main/subagents/types.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/desktop/src/main/chat.ts b/packages/desktop/src/main/chat.ts index 653c9bc5..3a56b27f 100644 --- a/packages/desktop/src/main/chat.ts +++ b/packages/desktop/src/main/chat.ts @@ -2000,12 +2000,12 @@ export class ChatService { const base = { cwd: () => cwd, authStorage: this.authStorage, - resolveModel: (key: string) => this.findModelByKey(key) ?? null, nameAllocator: () => allocator, modelRegistry: this.modelRegistry, + settingsManager: this.settingsManager, model: () => this.pickModel() ?? null, availableModels: () => this.workflowModels(), - settingsManager: this.settingsManager + resolveModel: (key: string) => this.findModelByKey(key) ?? null }; const subagentSessionTools = (): ReturnType => createStartCustomTools({ ...base, includeSubagents: false, customTools: subagentSessionTools }); @@ -2149,8 +2149,8 @@ export class ChatService { return [ { score, - key: modelKey(model), name: model.name, + key: modelKey(model), provider: model.provider, effortLevels: getSupportedEffortLevels(model) } diff --git a/packages/desktop/src/main/models.ts b/packages/desktop/src/main/models.ts index 2e044b28..1c4a388b 100644 --- a/packages/desktop/src/main/models.ts +++ b/packages/desktop/src/main/models.ts @@ -13,11 +13,11 @@ export interface AllowedModel { } export const models: readonly AllowedModel[] = [ - { provider: 'openai', id: 'gpt-5.5', score: { affordability: 9, intelligence: 8, taste: 5 } }, - { provider: 'openai', id: 'gpt-5.4', score: { affordability: 8, intelligence: 6, taste: 4 } }, - { provider: 'anthropic', id: 'claude-fable-5', score: { affordability: 2, intelligence: 9, taste: 9 } }, - { provider: 'anthropic', id: 'claude-opus-4-8', score: { affordability: 4, intelligence: 7, taste: 8 } }, - { provider: 'anthropic', id: 'claude-sonnet-5', score: { affordability: 5, intelligence: 5, taste: 7 } } + { provider: 'openai', id: 'gpt-5.5', score: { taste: 5, intelligence: 8, affordability: 9 } }, + { provider: 'openai', id: 'gpt-5.4', score: { taste: 4, intelligence: 6, affordability: 8 } }, + { provider: 'anthropic', id: 'claude-fable-5', score: { taste: 9, intelligence: 9, affordability: 2 } }, + { provider: 'anthropic', id: 'claude-opus-4-8', score: { taste: 8, intelligence: 7, affordability: 4 } }, + { provider: 'anthropic', id: 'claude-sonnet-5', score: { taste: 7, intelligence: 5, affordability: 5 } } ] as const; const scoresByKey = new Map(models.map((model) => [`${model.provider}:${model.id}`, model.score])); diff --git a/packages/desktop/src/main/subagents/types.ts b/packages/desktop/src/main/subagents/types.ts index 950a8e05..3dc30618 100644 --- a/packages/desktop/src/main/subagents/types.ts +++ b/packages/desktop/src/main/subagents/types.ts @@ -10,8 +10,8 @@ export interface SubagentTaskInput { export interface WorkflowModelOption { key: string; name: string; - score: ModelScore; provider: string; + score: ModelScore; effortLevels: EffortLevel[]; } From 3331a1cbcf7d72b2930e8e96b4283cc1c1eee8c3 Mon Sep 17 00:00:00 2001 From: sasicodes <29498872+sasicodes@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:46:00 +0530 Subject: [PATCH 4/8] chore: share ResolvedModel type across workflow modules --- packages/desktop/src/main/providers/tools/subagents.ts | 4 +--- packages/desktop/src/main/subagents/runtime.ts | 4 +--- packages/desktop/src/main/subagents/types.ts | 3 +++ 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/desktop/src/main/providers/tools/subagents.ts b/packages/desktop/src/main/providers/tools/subagents.ts index d1f7570d..49b38b5a 100644 --- a/packages/desktop/src/main/providers/tools/subagents.ts +++ b/packages/desktop/src/main/providers/tools/subagents.ts @@ -8,7 +8,7 @@ import { import { toolResult } from '@main/providers/tools/result'; import type { SubagentNameAllocator } from '@main/subagents/allocator'; import { runSubagents } from '@main/subagents/runtime'; -import type { WorkflowModelOption } from '@main/subagents/types'; +import type { ResolvedModel, WorkflowModelOption } from '@main/subagents/types'; import { workflowToolDescription } from '@main/subagents/utils/catalog'; import { maxSubagentTasks, normalizeSubagentTasks } from '@main/subagents/utils/input'; import { effortLevels } from '@main/types'; @@ -47,8 +47,6 @@ const spawnToolParameters = { additionalProperties: false } as const; -type ResolvedModel = ModelRegistry['getAvailable'] extends () => Array ? ModelItem : never; - interface CreateSubagentToolsOptions { cwd: () => string; authStorage: AuthStorage; diff --git a/packages/desktop/src/main/subagents/runtime.ts b/packages/desktop/src/main/subagents/runtime.ts index cb61c41d..5e8b8f07 100644 --- a/packages/desktop/src/main/subagents/runtime.ts +++ b/packages/desktop/src/main/subagents/runtime.ts @@ -13,11 +13,9 @@ import { agentEndError, clampThinkingLevel } from '@main/helpers'; import { createStartResourceLoader } from '@main/prompt/loader'; import type { SubagentNameAllocator } from '@main/subagents/allocator'; import { subagentAccentColor, subagentAvatar } from '@main/subagents/avatar'; -import type { SubagentRunResult, SubagentRunSnapshot, SubagentTaskInput } from '@main/subagents/types'; +import type { ResolvedModel, SubagentRunResult, SubagentRunSnapshot, SubagentTaskInput } from '@main/subagents/types'; import type { SubagentActivity } from '@main/types'; -type ResolvedModel = ModelRegistry['getAvailable'] extends () => Array ? ModelItem : never; - interface RunSubagentsOptions { cwd: string; signal?: AbortSignal; diff --git a/packages/desktop/src/main/subagents/types.ts b/packages/desktop/src/main/subagents/types.ts index 3dc30618..30bdc2fb 100644 --- a/packages/desktop/src/main/subagents/types.ts +++ b/packages/desktop/src/main/subagents/types.ts @@ -1,6 +1,9 @@ +import type { ModelRegistry } from '@earendil-works/pi-coding-agent'; import type { ModelScore } from '@main/models'; import type { EffortLevel, SubagentActivity } from '@main/types'; +export type ResolvedModel = ModelRegistry['getAvailable'] extends () => Array ? ModelItem : never; + export interface SubagentTaskInput { model: string; prompt: string; From 3e6b76e35329ae86b11df397d82145774877bf75 Mon Sep 17 00:00:00 2001 From: sasicodes <29498872+sasicodes@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:50:21 +0530 Subject: [PATCH 5/8] fix: match workflow model scores by id so subscriptions populate the menu --- packages/desktop/src/main/chat.ts | 4 ++-- packages/desktop/src/main/models.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/main/chat.ts b/packages/desktop/src/main/chat.ts index 3a56b27f..f31a8274 100644 --- a/packages/desktop/src/main/chat.ts +++ b/packages/desktop/src/main/chat.ts @@ -2143,8 +2143,8 @@ export class ChatService { } private workflowModels(): WorkflowModelOption[] { - return this.modelRegistry.getAvailable().flatMap((model) => { - const score = modelScore(model.provider, model.id); + return this.getPickerModels().flatMap((model) => { + const score = modelScore(model.id); if (!score) return []; return [ { diff --git a/packages/desktop/src/main/models.ts b/packages/desktop/src/main/models.ts index 1c4a388b..df5967ca 100644 --- a/packages/desktop/src/main/models.ts +++ b/packages/desktop/src/main/models.ts @@ -20,10 +20,9 @@ export const models: readonly AllowedModel[] = [ { provider: 'anthropic', id: 'claude-sonnet-5', score: { taste: 7, intelligence: 5, affordability: 5 } } ] as const; -const scoresByKey = new Map(models.map((model) => [`${model.provider}:${model.id}`, model.score])); +const scoresById = new Map(models.map((model) => [model.id, model.score])); -export const modelScore = (provider: string, id: string): ModelScore | undefined => - scoresByKey.get(`${provider}:${id}`); +export const modelScore = (id: string): ModelScore | undefined => scoresById.get(id); const idsByProvider = (provider: ProviderKey) => models.filter((model) => model.provider === provider).map((model) => model.id); From 21178a32c4267d9a9d7351dc8a206f120e04b069 Mon Sep 17 00:00:00 2001 From: sasicodes <29498872+sasicodes@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:54:56 +0530 Subject: [PATCH 6/8] fix: fall back to the configured model when a workflow task names an unavailable one --- packages/desktop/src/main/chat.ts | 2 +- packages/desktop/src/main/subagents/utils/catalog.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/main/chat.ts b/packages/desktop/src/main/chat.ts index f31a8274..269aaec7 100644 --- a/packages/desktop/src/main/chat.ts +++ b/packages/desktop/src/main/chat.ts @@ -2005,7 +2005,7 @@ export class ChatService { settingsManager: this.settingsManager, model: () => this.pickModel() ?? null, availableModels: () => this.workflowModels(), - resolveModel: (key: string) => this.findModelByKey(key) ?? null + resolveModel: (key: string) => this.findModelByKey(key) ?? this.pickModel() ?? null }; const subagentSessionTools = (): ReturnType => createStartCustomTools({ ...base, includeSubagents: false, customTools: subagentSessionTools }); diff --git a/packages/desktop/src/main/subagents/utils/catalog.ts b/packages/desktop/src/main/subagents/utils/catalog.ts index 6e86a6cc..cc336be7 100644 --- a/packages/desktop/src/main/subagents/utils/catalog.ts +++ b/packages/desktop/src/main/subagents/utils/catalog.ts @@ -10,5 +10,5 @@ export const workflowToolDescription = (options: WorkflowModelOption[]) => { return 'Run a workflow of focused sub-agents in parallel. No models are configured; set up a provider before spawning sub-agents.'; } - return `Run a workflow of focused sub-agents in parallel. Choose a model and effort per task from the scores below (0-10, higher is better; affordability higher means cheaper). Do not default to the highest scores, that wastes tokens. Pick the cheapest model and lowest effort that can still do the task well, and only move up when the task truly needs it: reading, lookups, and mechanical edits belong on a cheap model at low effort; design, UX, and architecture want higher taste; genuinely hard reasoning wants higher intelligence at high or xhigh effort. Match each task to the minimum capability it needs.\n\nModels:\n${workflowModelMenu(options)}`; + return `Run a workflow of focused sub-agents in parallel. For each task choose one of the model keys listed below exactly (only these models are configured) and an effort, using the scores (0-10, higher is better; affordability higher means cheaper). Do not default to the highest scores, that wastes tokens. Pick the cheapest model and lowest effort that can still do the task well, and only move up when the task truly needs it: reading, lookups, and mechanical edits belong on a cheap model at low effort; design, UX, and architecture want higher taste; genuinely hard reasoning wants higher intelligence at high or xhigh effort. Match each task to the minimum capability it needs.\n\nModels:\n${workflowModelMenu(options)}`; }; From 2153f986d10696f9adfa78c0049e9b7139b8ad92 Mon Sep 17 00:00:00 2001 From: sasicodes <29498872+sasicodes@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:02:49 +0530 Subject: [PATCH 7/8] fix: clarify workflow error when no provider is configured --- packages/desktop/src/main/subagents/runtime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/main/subagents/runtime.ts b/packages/desktop/src/main/subagents/runtime.ts index 5e8b8f07..22076c8b 100644 --- a/packages/desktop/src/main/subagents/runtime.ts +++ b/packages/desktop/src/main/subagents/runtime.ts @@ -125,7 +125,7 @@ export const runSubagents = async ({ let session: AgentSession | null = null; try { const model = resolveModel(task.model); - if (!model) throw new Error(`Model ${task.model} is not available.`); + if (!model) throw new Error('No configured model is available. Set up a provider in settings.'); agent.status = 'running'; update(); From 082a4eb46a73d547a07f6a6ca62830cd0d624efb Mon Sep 17 00:00:00 2001 From: sasicodes <29498872+sasicodes@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:17:41 +0530 Subject: [PATCH 8/8] fix: keep workflow model menu fresh as providers change --- packages/desktop/src/main/providers/tools/subagents.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/main/providers/tools/subagents.ts b/packages/desktop/src/main/providers/tools/subagents.ts index 49b38b5a..6bfc7479 100644 --- a/packages/desktop/src/main/providers/tools/subagents.ts +++ b/packages/desktop/src/main/providers/tools/subagents.ts @@ -73,7 +73,9 @@ export const createSubagentTools = ({ name: 'run_workflow', executionMode: 'sequential', parameters: spawnToolParameters, - description: workflowToolDescription(availableModels()), + get description() { + return workflowToolDescription(availableModels()); + }, promptSnippet: 'Use for independent research, review, or mapping work.', prepareArguments: (args) => ({ tasks: normalizeSubagentTasks(args) }), async execute(_toolCallId, { tasks }, signal, onUpdate) {