From 204f7c82d13d87eb9334085184a0e1f974d048c1 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Sun, 27 Sep 2026 04:59:46 -0700 Subject: [PATCH 1/4] fix(flows): pin generated Claude agent model Session-Id: 01a0e2b3-ef42-7541-bdce-dcd6b88caecc Session-Id: 01a0e2c3-b5bd-7631-b137-02be9af83c2e --- web/lib/flow-agent-settings.ts | 14 +++++++++++++- web/lib/test/flow-agent-settings.test.ts | 16 ++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/web/lib/flow-agent-settings.ts b/web/lib/flow-agent-settings.ts index 94b27bf..590db01 100644 --- a/web/lib/flow-agent-settings.ts +++ b/web/lib/flow-agent-settings.ts @@ -6,6 +6,16 @@ export type AgentRole = typeof AGENT_ROLES[number]; export type AgentSettings = { agent?: AgentId; model?: string; prompt?: string }; export type FlowAgentSettings = Partial>; +/** + * Models the generated flow can rely on without inheriting a moving CLI + * default. Claude is the only supported harness whose Relayflow adapter picks + * a model on the flow's behalf; pinning it here keeps preflight aligned with + * the model shown in the editor and sent in every generated agent step. + */ +const DEFAULT_AGENT_MODELS: Partial> = { + claude: 'claude-sonnet-4-6', +}; + export function rolesForStep(step: WorkflowStep): AgentRole[] { if (step === '3 implementations') return ['prototype-1', 'prototype-2', 'prototype-3']; if (step === '2× adversarial review') return ['adversary', 'fixer']; @@ -38,7 +48,9 @@ export function resolveAgentSettings(workflow: WorkflowId, role: AgentRole, sele // Changing the selected agents must never leave an unavailable CLI assigned. const agent = saved?.agent && isCodingAgent(saved.agent) && available.includes(saved.agent) ? saved.agent : defaultAgent; const compatible = !saved?.agent || saved.agent === agent; - return { agent, model: compatible ? saved?.model?.trim() || '' : '', prompt: saved?.prompt ?? defaultAgentPrompt(workflow, role) }; + const defaultModel = DEFAULT_AGENT_MODELS[agent] ?? ''; + const model = compatible ? saved?.model?.trim() || defaultModel : defaultModel; + return { agent, model, prompt: saved?.prompt ?? defaultAgentPrompt(workflow, role) }; } export function validFlowAgentSettings(value: unknown): value is FlowAgentSettings { diff --git a/web/lib/test/flow-agent-settings.test.ts b/web/lib/test/flow-agent-settings.test.ts index 739b6ab..2855ba3 100644 --- a/web/lib/test/flow-agent-settings.test.ts +++ b/web/lib/test/flow-agent-settings.test.ts @@ -26,13 +26,21 @@ describe('per-step agent settings', () => { expect(resolveAgentSettings('traditional', 'planner', ['grok', 'cursor'])).toMatchObject({ agent: 'grok', model: '' }); expect(resolveAgentSettings('traditional', 'adversary', ['grok', 'cursor'])).toMatchObject({ agent: 'cursor', model: '' }); expect(resolveAgentSettings('prototype', 'prototype-2', ['codex']).agent).toBe('codex'); - expect(resolveAgentSettings('simple', 'implementer', ['claude'], { 'simple:implementer': { agent: 'grok', model: 'grok-model', prompt: 'Custom work' } })).toMatchObject({ agent: 'claude', model: '', prompt: 'Custom work' }); + expect(resolveAgentSettings('simple', 'implementer', ['claude'], { 'simple:implementer': { agent: 'grok', model: 'grok-model', prompt: 'Custom work' } })).toMatchObject({ agent: 'claude', model: 'claude-sonnet-4-6', prompt: 'Custom work' }); }); it('keeps old OpenCode settings readable while falling back to a supported agent', () => { const saved = { 'simple:implementer': { agent: 'opencode' as const, model: 'opencode-model', prompt: 'Custom work' } }; expect(readFactoryDraft(JSON.stringify({ ...draft, agentSettings: saved })))?.toMatchObject({ agentSettings: saved }); - expect(resolveAgentSettings('simple', 'implementer', ['opencode'], saved)).toMatchObject({ agent: 'claude', model: '', prompt: 'Custom work' }); + expect(resolveAgentSettings('simple', 'implementer', ['opencode'], saved)).toMatchObject({ agent: 'claude', model: 'claude-sonnet-4-6', prompt: 'Custom work' }); + }); + + it('pins every Claude step in the simple prebuilt flow to a probeable model', async () => { + const calls = await execute({ ...draft, agents: ['claude'], workflow: 'simple' }); + expect(Object.keys(calls)).toEqual(['check-discovery', 'implementer', 'check-repair-1', 'check-repair-2']); + for (const options of Object.values(calls)) { + expect(options).toMatchObject({ cli: 'claude', model: 'claude-sonnet-4-6' }); + } }); it('runs distinct prototype overrides while preserving ticket and worktree context', async () => { @@ -48,7 +56,7 @@ describe('per-step agent settings', () => { expect(calls['prototype-1'].task).toContain('Ticket title\nTicket body\nKeep changes focused.'); expect(calls['prototype-1'].task).toContain('Assigned approach: the smallest change'); expect(calls['prototype-2'].model).toBe('model-two'); - expect(calls['prototype-3'].model).toBeUndefined(); + expect(calls['prototype-3'].model).toBe('claude-sonnet-4-6'); expect(calls.comparator.task).toContain('/tmp/prototypes/1, /tmp/prototypes/2, /tmp/prototypes/3'); expect(calls.implementer).toMatchObject({ cli: 'cursor', model: 'build-model' }); }); @@ -56,7 +64,7 @@ describe('per-step agent settings', () => { it('applies the shared reviewer settings to both traditional rounds', async () => { const calls = await execute({ ...draft, workflow: 'traditional', agentSettings: { 'traditional:adversary': { agent: 'grok', model: 'review-model', prompt: 'Check the diff. Write review.clean only if clean.' } } }); for (const role of ['adversary-1', 'adversary-2']) expect(calls[role]).toMatchObject({ cli: 'grok', model: 'review-model' }); - expect(calls.planner.model).toBeUndefined(); + expect(calls.planner.model).toBe('claude-sonnet-4-6'); }); it('persists valid overrides and includes them in both handoff sources', () => { From d8905535769b18d1ae31c105b02cfb61882f4887 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Sun, 27 Sep 2026 07:50:33 -0700 Subject: [PATCH 2/4] test(flows): enforce generated agent model contract Session-Id: 01a0e2c3-b5bd-7631-b137-02be9af83c2e --- web/lib/flow-agent-settings.ts | 17 ++-- web/lib/flow-workflows.ts | 4 +- web/lib/test/flow-agent-settings.test.ts | 107 ++++++++++++++++++++--- 3 files changed, 109 insertions(+), 19 deletions(-) diff --git a/web/lib/flow-agent-settings.ts b/web/lib/flow-agent-settings.ts index 590db01..6f3a956 100644 --- a/web/lib/flow-agent-settings.ts +++ b/web/lib/flow-agent-settings.ts @@ -7,13 +7,18 @@ export type AgentSettings = { agent?: AgentId; model?: string; prompt?: string } export type FlowAgentSettings = Partial>; /** - * Models the generated flow can rely on without inheriting a moving CLI - * default. Claude is the only supported harness whose Relayflow adapter picks - * a model on the flow's behalf; pinning it here keeps preflight aligned with - * the model shown in the editor and sent in every generated agent step. + * Stable defaults for first-party generated flows. These identifiers are + * verified against the current CLI model catalogs/readiness probes; the + * runtime still proves the exact credential/model pair before agent work. + * + * Keep this exhaustive so enabling another generator agent cannot silently + * reintroduce an omitted model and inherit an adapter default. */ -const DEFAULT_AGENT_MODELS: Partial> = { +export const DEFAULT_AGENT_MODELS: Readonly> = { claude: 'claude-sonnet-4-6', + codex: 'gpt-5.5', + cursor: 'gpt-5.3-codex', + grok: 'grok-4.6', }; export function rolesForStep(step: WorkflowStep): AgentRole[] { @@ -48,7 +53,7 @@ export function resolveAgentSettings(workflow: WorkflowId, role: AgentRole, sele // Changing the selected agents must never leave an unavailable CLI assigned. const agent = saved?.agent && isCodingAgent(saved.agent) && available.includes(saved.agent) ? saved.agent : defaultAgent; const compatible = !saved?.agent || saved.agent === agent; - const defaultModel = DEFAULT_AGENT_MODELS[agent] ?? ''; + const defaultModel = DEFAULT_AGENT_MODELS[agent]; const model = compatible ? saved?.model?.trim() || defaultModel : defaultModel; return { agent, model, prompt: saved?.prompt ?? defaultAgentPrompt(workflow, role) }; } diff --git a/web/lib/flow-workflows.ts b/web/lib/flow-workflows.ts index 0fcf678..99e9afa 100644 --- a/web/lib/flow-workflows.ts +++ b/web/lib/flow-workflows.ts @@ -640,7 +640,7 @@ export function workflowCode(workflow: WorkflowId, agents: ReturnType "'" + value.replace(/'/g, "'\\\\''") + "'"; const prototypeAgents = ${JSON.stringify(prototypeConfigs.map(value => value.agent))}; - const prototypeSettings: { model?: string; prompt: string }[] = ${JSON.stringify(prototypeConfigs.map(({ model, prompt }) => ({ ...(model ? { model } : {}), prompt })))}; + const prototypeSettings: { model: string; prompt: string }[] = ${JSON.stringify(prototypeConfigs.map(({ model, prompt }) => ({ model, prompt })))}; const approaches = ["the smallest change", "a maintainable design", "a different approach"]; const paths = approaches.map((_, index) => prototypeRoot + "/" + (index + 1)); const base = (await f.run("git rev-parse HEAD")).trim(); @@ -650,7 +650,7 @@ export function workflowCode(workflow: WorkflowId, agents: ReturnType f.agent("prototype-" + (index + 1), { cli: prototypeAgents[index], cwd, - ...(prototypeSettings[index].model ? { model: prototypeSettings[index].model } : {}), + model: prototypeSettings[index].model, task: task + "\\n" + prototypeSettings[index].prompt + " Assigned approach: " + approaches[index], }))); // All three implementations are finished before comparison begins. diff --git a/web/lib/test/flow-agent-settings.test.ts b/web/lib/test/flow-agent-settings.test.ts index 2855ba3..a282fc3 100644 --- a/web/lib/test/flow-agent-settings.test.ts +++ b/web/lib/test/flow-agent-settings.test.ts @@ -1,31 +1,65 @@ import { describe, expect, it } from 'vitest'; import ts from 'typescript'; import { DEFAULT_FACTORY, factorySource, readFactoryDraft, cloudConnectionsHref, type FactoryDraft } from '../flow-onboarding'; -import { resolveAgentSettings } from '../flow-agent-settings'; +import { DEFAULT_AGENT_MODELS, resolveAgentSettings } from '../flow-agent-settings'; import { localKitFiles } from '../flow-local'; -import { FLOW_VALIDATE_CHANGE_METADATA_COMMAND } from '../flow-workflows'; +import { FLOW_CHECK_RUN_COMMAND, FLOW_VALIDATE_CHANGE_METADATA_COMMAND } from '../flow-workflows'; +import type { CodingAgent } from '../flow-agents'; const draft: FactoryDraft = { ...DEFAULT_FACTORY, sources: ['github'], agents: ['claude', 'codex', 'grok', 'cursor'], workflow: 'prototype', step: 3 }; -async function execute(value: FactoryDraft) { - const source = factorySource(value).replace('import { flow } from "@relayflows/surface";', ''); +type AgentOptions = { cli: CodingAgent; model?: string; task: string; cwd?: string }; + +async function execute(value: FactoryDraft, target: 'cloud' | 'local' = 'cloud') { + const source = factorySource(value, target).replace('import { flow } from "@relayflows/surface";', ''); const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }); const exports: { default?: (ctx: unknown, input: unknown) => Promise } = {}; new Function('exports', 'flow', compiled.outputText)(exports, (_name: string, _options: unknown, body: unknown) => body); - const calls: Record = {}; - await exports.default!({ agent: async (name: string, options: typeof calls[string]) => { calls[name] = options; }, + const calls: Record = {}; + let checkRuns = 0; + let reviewChecks = 0; + await exports.default!({ agent: async (name: string, options: AgentOptions) => { calls[name] = options; }, // "base=..." is the publish check; without a verdict it understands, the // flow correctly stops before the reviews rather than opening a pull // request for work that was never committed. - run: async (command: string) => command.endsWith(FLOW_VALIDATE_CHANGE_METADATA_COMMAND) ? 'valid' : command.startsWith('base=') ? 'publish' : command.startsWith('mktemp') ? '/tmp/prototypes' : command.includes('review.clean &&') ? 'yes' : 'base', done: () => {} }, - { issue: { source: 'github', title: 'Ticket title', body: 'Ticket body', labels: [], identifier: '#507' } }); + run: async (command: string) => { + if (command.endsWith(FLOW_VALIDATE_CHANGE_METADATA_COMMAND)) return 'valid'; + if (command === FLOW_CHECK_RUN_COMMAND) return ++checkRuns <= 2 ? 'fail' : 'pass'; + if (command.startsWith('base=')) return 'publish'; + if (command.startsWith('mktemp')) return '/tmp/prototypes'; + if (command.includes('review.clean &&')) return ++reviewChecks === 1 ? 'no' : 'yes'; + return 'base'; + }, done: () => {} }, + { issue: { source: value.sources[0] ?? 'github', title: 'Ticket title', body: 'Ticket body', labels: [], identifier: value.sources[0] === 'github' ? '#507' : undefined } }); + return calls; +} + +function agentObjectProperties(source: string): string[][] { + const file = ts.createSourceFile('software-factory.flow.ts', source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS); + const calls: string[][] = []; + const visit = (node: ts.Node) => { + if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && + node.expression.name.text === 'agent' && ts.isObjectLiteralExpression(node.arguments[1])) { + calls.push(node.arguments[1].properties.flatMap(property => ts.isPropertyAssignment(property) ? [property.name.getText(file)] : [])); + } + ts.forEachChild(node, visit); + }; + visit(file); return calls; } +function expectSupportedCalls(calls: Record) { + expect(Object.keys(calls).length).toBeGreaterThan(0); + for (const [name, options] of Object.entries(calls)) { + expect(options.cli, `${name} CLI`).toBeTruthy(); + expect(options.model, `${name} model`).toBe(DEFAULT_AGENT_MODELS[options.cli]); + } +} + describe('per-step agent settings', () => { it('inherits CLI models and adapts assignments to the selected agents', () => { - expect(resolveAgentSettings('traditional', 'planner', ['grok', 'cursor'])).toMatchObject({ agent: 'grok', model: '' }); - expect(resolveAgentSettings('traditional', 'adversary', ['grok', 'cursor'])).toMatchObject({ agent: 'cursor', model: '' }); - expect(resolveAgentSettings('prototype', 'prototype-2', ['codex']).agent).toBe('codex'); + expect(resolveAgentSettings('traditional', 'planner', ['grok', 'cursor'])).toMatchObject({ agent: 'grok', model: 'grok-4.6' }); + expect(resolveAgentSettings('traditional', 'adversary', ['grok', 'cursor'])).toMatchObject({ agent: 'cursor', model: 'gpt-5.3-codex' }); + expect(resolveAgentSettings('prototype', 'prototype-2', ['codex'])).toMatchObject({ agent: 'codex', model: 'gpt-5.5' }); expect(resolveAgentSettings('simple', 'implementer', ['claude'], { 'simple:implementer': { agent: 'grok', model: 'grok-model', prompt: 'Custom work' } })).toMatchObject({ agent: 'claude', model: 'claude-sonnet-4-6', prompt: 'Custom work' }); }); @@ -43,6 +77,57 @@ describe('per-step agent settings', () => { } }); + it('gives every generated preset agent an explicit supported CLI/model pair', async () => { + const workflows = ['simple', 'traditional', 'prototype'] as const; + const targets = ['cloud', 'local'] as const; + const sources = ['github', 'slack'] as const; + const selections: CodingAgent[][] = [['claude'], ['codex'], ['claude', 'codex'], ['cursor'], ['grok']]; + + for (const workflow of workflows) { + for (const target of targets) { + for (const source of sources) { + for (const agents of selections) { + const value: FactoryDraft = { ...draft, workflow, sources: [source], sourceSettings: {}, agents }; + const generated = factorySource(value, target); + const objects = agentObjectProperties(generated); + expect(objects.length, `${workflow}/${target}/${source}/${agents.join('+')}`).toBeGreaterThan(0); + for (const properties of objects) { + expect(properties).toContain('cli'); + expect(properties).toContain('model'); + } + expectSupportedCalls(await execute(value, target)); + } + } + } + } + }); + + it('keeps Cloud handoff and local-kit source on the same explicit model contract', () => { + for (const workflow of ['simple', 'traditional', 'prototype'] as const) { + for (const source of ['github', 'slack'] as const) { + for (const agents of [['claude'], ['codex'], ['claude', 'codex'], ['cursor'], ['grok']] as CodingAgent[][]) { + const value: FactoryDraft = { ...draft, workflow, sources: [source], sourceSettings: {}, agents }; + const handoff = JSON.parse(decodeURIComponent(new URL(cloudConnectionsHref(value, 'model-contract')).hash.slice(1))) as { source: string }; + expect(handoff.source).toBe(factorySource(value, 'cloud')); + expect(localKitFiles(value)['software-factory.flow.mts']).toBe(factorySource(value, 'local')); + for (const generated of [handoff.source, localKitFiles(value)['software-factory.flow.mts']]) { + for (const properties of agentObjectProperties(generated)) { + expect(properties).toContain('cli'); + expect(properties).toContain('model'); + } + } + } + } + } + }); + + it('materializes the model on the third Simple agent in the failed agent-3 topology', () => { + const source = factorySource({ ...draft, workflow: 'simple', agents: ['claude'], sources: ['slack'], sourceSettings: {} }); + const objects = agentObjectProperties(source); + expect(objects[2]).toEqual(expect.arrayContaining(['cli', 'model'])); + expect(source).not.toContain('claude-opus-5'); + }); + it('runs distinct prototype overrides while preserving ticket and worktree context', async () => { const prompt = 'Use "quotes", `ticks`, ${literal}, and a newline.\nWrite prototype-notes.md.'; const calls = await execute({ ...draft, task: 'Keep changes focused.', agentSettings: { From b5ce9b0e48730420fdbfd2057fc5a9688d810c23 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Sun, 27 Sep 2026 07:53:26 -0700 Subject: [PATCH 3/4] fix(flows): preserve custom model telemetry Session-Id: 01a0e2c3-b5bd-7631-b137-02be9af83c2e --- web/app/flows/onboarding/AgentStepEditor.tsx | 4 ++-- web/lib/flow-agent-settings.ts | 7 +++++++ web/lib/test/flow-agent-settings.test.ts | 9 ++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/web/app/flows/onboarding/AgentStepEditor.tsx b/web/app/flows/onboarding/AgentStepEditor.tsx index ef213ef..95e818e 100644 --- a/web/app/flows/onboarding/AgentStepEditor.tsx +++ b/web/app/flows/onboarding/AgentStepEditor.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { X } from 'lucide-react'; import { agentLabel, isCodingAgent, type FactoryDraft } from '../../../lib/flow-onboarding'; -import { defaultAgentPrompt, resolveAgentSettings, type AgentRole, type FlowAgentSettings } from '../../../lib/flow-agent-settings'; +import { defaultAgentPrompt, hasCustomAgentModel, resolveAgentSettings, type AgentRole, type FlowAgentSettings } from '../../../lib/flow-agent-settings'; import type { FlowTrack } from '../../../lib/flow-analytics'; import s from './onboarding.module.css'; @@ -22,7 +22,7 @@ export function AgentStepEditor({ draft, roles, onChange, onClose, onTrack }: { onChange({ ...draft, agentSettings: settings }); for (const id of roles) { const value = resolveAgentSettings(workflow, id, draft.agents, settings); - onTrack('agent_settings_saved', { role: id, agent: value.agent, custom_model: Boolean(value.model), custom_prompt: value.prompt !== defaultAgentPrompt(workflow, id) }); + onTrack('agent_settings_saved', { role: id, agent: value.agent, custom_model: hasCustomAgentModel(workflow, id, draft.agents, settings), custom_prompt: value.prompt !== defaultAgentPrompt(workflow, id) }); } dialog.current?.close(); } diff --git a/web/lib/flow-agent-settings.ts b/web/lib/flow-agent-settings.ts index 6f3a956..72ad3d2 100644 --- a/web/lib/flow-agent-settings.ts +++ b/web/lib/flow-agent-settings.ts @@ -58,6 +58,13 @@ export function resolveAgentSettings(workflow: WorkflowId, role: AgentRole, sele return { agent, model, prompt: saved?.prompt ?? defaultAgentPrompt(workflow, role) }; } +/** Whether this role has an effective user-entered model override. */ +export function hasCustomAgentModel(workflow: WorkflowId, role: AgentRole, selected: readonly string[], settings: FlowAgentSettings = {}): boolean { + const saved = settings[`${workflow}:${role}`]; + if (!saved?.model?.trim()) return false; + return !saved.agent || saved.agent === resolveAgentSettings(workflow, role, selected, settings).agent; +} + export function validFlowAgentSettings(value: unknown): value is FlowAgentSettings { if (value === undefined) return true; if (!value || typeof value !== 'object' || Array.isArray(value)) return false; diff --git a/web/lib/test/flow-agent-settings.test.ts b/web/lib/test/flow-agent-settings.test.ts index a282fc3..509d8c2 100644 --- a/web/lib/test/flow-agent-settings.test.ts +++ b/web/lib/test/flow-agent-settings.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import ts from 'typescript'; import { DEFAULT_FACTORY, factorySource, readFactoryDraft, cloudConnectionsHref, type FactoryDraft } from '../flow-onboarding'; -import { DEFAULT_AGENT_MODELS, resolveAgentSettings } from '../flow-agent-settings'; +import { DEFAULT_AGENT_MODELS, hasCustomAgentModel, resolveAgentSettings } from '../flow-agent-settings'; import { localKitFiles } from '../flow-local'; import { FLOW_CHECK_RUN_COMMAND, FLOW_VALIDATE_CHANGE_METADATA_COMMAND } from '../flow-workflows'; import type { CodingAgent } from '../flow-agents'; @@ -69,6 +69,13 @@ describe('per-step agent settings', () => { expect(resolveAgentSettings('simple', 'implementer', ['opencode'], saved)).toMatchObject({ agent: 'claude', model: 'claude-sonnet-4-6', prompt: 'Custom work' }); }); + it('distinguishes generated defaults from compatible custom-model telemetry', () => { + expect(hasCustomAgentModel('simple', 'implementer', ['claude'])).toBe(false); + expect(hasCustomAgentModel('simple', 'implementer', ['claude'], { 'simple:implementer': { model: ' ' } })).toBe(false); + expect(hasCustomAgentModel('simple', 'implementer', ['claude'], { 'simple:implementer': { agent: 'claude', model: 'custom-model' } })).toBe(true); + expect(hasCustomAgentModel('simple', 'implementer', ['claude'], { 'simple:implementer': { agent: 'grok', model: 'grok-4.6' } })).toBe(false); + }); + it('pins every Claude step in the simple prebuilt flow to a probeable model', async () => { const calls = await execute({ ...draft, agents: ['claude'], workflow: 'simple' }); expect(Object.keys(calls)).toEqual(['check-discovery', 'implementer', 'check-repair-1', 'check-repair-2']); From cadc1cd32a267c881c5bae1638b79d3f46e70aa4 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Sun, 27 Sep 2026 08:03:40 -0700 Subject: [PATCH 4/4] fix(flows): use current generated model defaults Session-Id: 01a0e2c3-b5bd-7631-b137-02be9af83c2e --- web/app/flows/onboarding/AgentStepEditor.tsx | 8 ++--- web/app/flows/onboarding/WorkflowPicker.tsx | 6 ++-- web/lib/flow-agent-settings.ts | 20 +++++------ web/lib/flow-preview.ts | 6 ++-- web/lib/flow-workflows.ts | 4 +-- web/lib/test/flow-agent-settings.test.ts | 35 +++++++++++--------- web/lib/test/flow-preview.test.ts | 6 ++++ 7 files changed, 47 insertions(+), 38 deletions(-) diff --git a/web/app/flows/onboarding/AgentStepEditor.tsx b/web/app/flows/onboarding/AgentStepEditor.tsx index 95e818e..793cc67 100644 --- a/web/app/flows/onboarding/AgentStepEditor.tsx +++ b/web/app/flows/onboarding/AgentStepEditor.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { X } from 'lucide-react'; import { agentLabel, isCodingAgent, type FactoryDraft } from '../../../lib/flow-onboarding'; -import { defaultAgentPrompt, hasCustomAgentModel, resolveAgentSettings, type AgentRole, type FlowAgentSettings } from '../../../lib/flow-agent-settings'; +import { DEFAULT_AGENT_MODELS, defaultAgentPrompt, resolveAgentSettings, type AgentRole, type FlowAgentSettings } from '../../../lib/flow-agent-settings'; import type { FlowTrack } from '../../../lib/flow-analytics'; import s from './onboarding.module.css'; @@ -22,7 +22,7 @@ export function AgentStepEditor({ draft, roles, onChange, onClose, onTrack }: { onChange({ ...draft, agentSettings: settings }); for (const id of roles) { const value = resolveAgentSettings(workflow, id, draft.agents, settings); - onTrack('agent_settings_saved', { role: id, agent: value.agent, custom_model: hasCustomAgentModel(workflow, id, draft.agents, settings), custom_prompt: value.prompt !== defaultAgentPrompt(workflow, id) }); + onTrack('agent_settings_saved', { role: id, agent: value.agent, custom_model: Boolean(value.model), custom_prompt: value.prompt !== defaultAgentPrompt(workflow, id) }); } dialog.current?.close(); } @@ -40,9 +40,9 @@ export function AgentStepEditor({ draft, roles, onChange, onClose, onTrack }: { - + -

Leave the model blank to use {agentLabel(current.agent)}’s configured model, or enter a model ID available to your account.

+

Leave blank to pin {DEFAULT_AGENT_MODELS[current.agent]} in the generated flow, or enter a different model ID available to your account.

ReasoningAgent default

Uses your agent’s reasoning settings. The flow runner doesn’t support overriding reasoning per step yet.