Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions web/app/flows/onboarding/AgentStepEditor.tsx
Original file line number Diff line number Diff line change
@@ -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 { 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';

Expand Down Expand Up @@ -40,9 +40,9 @@ export function AgentStepEditor({ draft, roles, onChange, onClose, onTrack }: {
<label>Agent<select value={current.agent} onChange={event => setSettings({ ...settings, [key]: { ...current, agent: event.target.value as typeof current.agent, model: '' } })}>
{choices.map(id => <option key={id} value={id}>{agentLabel(id)}</option>)}
</select></label>
<label>Model<input type="text" maxLength={120} value={current.model} placeholder="Agent default" aria-describedby="agent-model-help" onChange={event => setSettings({ ...settings, [key]: { ...current, model: event.target.value } })} /></label>
<label>Model<input type="text" maxLength={120} value={current.model} placeholder={DEFAULT_AGENT_MODELS[current.agent]} aria-describedby="agent-model-help" onChange={event => setSettings({ ...settings, [key]: { ...current, model: event.target.value } })} /></label>
</div>
<p id="agent-model-help" className={s.agentEditorHint}>Leave the model blank to use {agentLabel(current.agent)}’s configured model, or enter a model ID available to your account.</p>
<p id="agent-model-help" className={s.agentEditorHint}>Leave blank to pin {DEFAULT_AGENT_MODELS[current.agent]} in the generated flow, or enter a different model ID available to your account.</p>
<div className={s.agentReasoning}><span>Reasoning</span><strong>Agent default</strong></div>
<p className={s.agentEditorHint}>Uses your agent’s reasoning settings. The flow runner doesn’t support overriding reasoning per step yet.</p>
<label className={s.agentPrompt}>Step prompt<textarea rows={8} maxLength={6000} required value={current.prompt} onChange={event => setSettings({ ...settings, [key]: { ...current, prompt: event.target.value } })} aria-describedby="agent-prompt-help" /></label>
Expand Down
6 changes: 3 additions & 3 deletions web/app/flows/onboarding/WorkflowPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AgentStepEditor } from './AgentStepEditor';
import { resolveAgentSettings, rolesForStep, type AgentRole } from '../../../lib/flow-agent-settings';
import { resolveGeneratedAgentSettings, rolesForStep, type AgentRole } from '../../../lib/flow-agent-settings';
import { GitBranch, Layers3, Zap, UserRound, LockKeyhole, Terminal, Settings } from 'lucide-react';
import { useState } from 'react';
import { WORKFLOWS, WORKFLOW_STEP_DETAILS, workflowAgents } from '../../../lib/flow-workflows';
Expand Down Expand Up @@ -92,7 +92,7 @@ export function WorkflowPlan({ draft, onChange, onTrack }: { draft: FactoryDraft
</li>}
{selectedWorkflow?.steps.map(step => {
const roles = rolesForStep(step);
const configs = roles.map(role => resolveAgentSettings(selectedWorkflow.id, role, draft.agents, draft.agentSettings));
const configs = roles.map(role => resolveGeneratedAgentSettings(selectedWorkflow.id, role, draft.agents, draft.agentSettings));
const nodeConfigs = step === '2× adversarial review' ? configs.slice(0, 1) : configs;
const agents = nodeConfigs.map(value => value.agent);
const Node = agents.length ? 'button' : 'div';
Expand All @@ -109,7 +109,7 @@ export function WorkflowPlan({ draft, onChange, onTrack }: { draft: FactoryDraft
<span className={s.processText}>
<span className={s.processHeading}><strong>{human ? 'Your approval' : review ? 'Adversarial review' : step}</strong><small className={s.processOwner}>{human ? 'You' : script ? 'Script' : step === '3 implementations' ? 'Parallel' : 'Agent'}</small></span>
<span className={s.processDescription}>{WORKFLOW_STEP_DETAILS[step]}</span>
{agents.length > 0 && <small className={s.processModel}><span>{nodeConfigs.length > 1 ? 'Configure 3 agents' : nodeConfigs[0].model || 'Default model'}</span><Settings size={12} aria-hidden="true" /></small>}
{agents.length > 0 && <small className={s.processModel}><span>{nodeConfigs.length > 1 ? 'Configure 3 agents' : nodeConfigs[0].model}</span><Settings size={12} aria-hidden="true" /></small>}
</span>
{review && <span className={s.processRounds}>2 rounds</span>}
{human && <LockKeyhole size={15} className={s.processLock} aria-hidden="true" />}
Expand Down
24 changes: 23 additions & 1 deletion web/lib/flow-agent-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ export type AgentRole = typeof AGENT_ROLES[number];
export type AgentSettings = { agent?: AgentId; model?: string; prompt?: string };
export type FlowAgentSettings = Partial<Record<`${WorkflowId}:${AgentRole}`, AgentSettings>>;

/**
* 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.
*/
export const DEFAULT_AGENT_MODELS: Readonly<Record<CodingAgent, string>> = {
claude: 'claude-sonnet-5',
codex: 'gpt-5.6-sol',
cursor: 'gpt-5.6-sol-high',
grok: 'grok-4.7',
Comment thread
khaliqgant marked this conversation as resolved.
};

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'];
Expand Down Expand Up @@ -38,7 +53,14 @@ 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 model = compatible ? saved?.model?.trim() || '' : '';
return { agent, model, prompt: saved?.prompt ?? defaultAgentPrompt(workflow, role) };
}

/** Resolve the explicit pair emitted by a first-party generated flow. */
export function resolveGeneratedAgentSettings(workflow: WorkflowId, role: AgentRole, selected: readonly string[], settings: FlowAgentSettings = {}) {
const value = resolveAgentSettings(workflow, role, selected, settings);
return { ...value, model: value.model || DEFAULT_AGENT_MODELS[value.agent] };
}

export function validFlowAgentSettings(value: unknown): value is FlowAgentSettings {
Expand Down
6 changes: 3 additions & 3 deletions web/lib/flow-preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CODING_AGENTS } from './flow-agents';
import { resolveAgentSettings, rolesForStep } from './flow-agent-settings';
import { resolveGeneratedAgentSettings, rolesForStep } from './flow-agent-settings';
import type { FactoryDraft } from './flow-onboarding';
import { repositoryHost, sourceLabel, sourceSummary } from './flow-sources';
import { WORKFLOWS, WORKFLOW_STEP_DETAILS } from './flow-workflows';
Expand All @@ -21,12 +21,12 @@ export function flowPreview(draft: FactoryDraft) {
owner: 'Trigger', detail: draft.sources.length > 1 ? 'Any one source can start the flow.' : '', icons: draft.sources,
},
...workflow.steps.map(step => {
const configs = rolesForStep(step).map(role => resolveAgentSettings(workflow.id, role, draft.agents, draft.agentSettings));
const configs = rolesForStep(step).map(role => resolveGeneratedAgentSettings(workflow.id, role, draft.agents, draft.agentSettings));
const human = step === 'Human gate';
const review = step === '2× adversarial review';
const agents = (review ? configs.slice(0, 1) : configs).map(config => ({
id: config.agent, label: CODING_AGENTS.find(agent => agent.id === config.agent)!.label,
model: config.model || 'Default model',
model: config.model,
}));
return {
kind: human ? 'approval' : configs.length ? 'agent' : 'script',
Expand Down
8 changes: 4 additions & 4 deletions web/lib/flow-workflows.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { resolveAgentSettings, type AgentRole, type FlowAgentSettings } from './flow-agent-settings';
import { resolveGeneratedAgentSettings, type AgentRole, type FlowAgentSettings } from './flow-agent-settings';
import { isCodingAgent } from './flow-agents';

export const WORKFLOWS = [
Expand Down Expand Up @@ -586,7 +586,7 @@ export function workflowAgents(selected: readonly string[]) {
}

export function workflowCode(workflow: WorkflowId, agents: ReturnType<typeof workflowAgents>, instructions: string, _target: 'cloud' | 'local' = 'cloud', settings: FlowAgentSettings = {}, selected: readonly string[] = [agents.builder, agents.reviewer]) {
const config = (role: AgentRole) => resolveAgentSettings(workflow, role, selected, settings);
const config = (role: AgentRole) => resolveGeneratedAgentSettings(workflow, role, selected, settings);
const options = (role: AgentRole, fallback: string, context = '') => {
const value = config(role);
const cli = value.agent === agents.builder && fallback === 'builder' ? 'builder' : JSON.stringify(value.agent);
Expand Down Expand Up @@ -640,7 +640,7 @@ export function workflowCode(workflow: WorkflowId, agents: ReturnType<typeof wor
const prototypeRoot = (await f.run("mktemp -d /tmp/relay-prototypes.XXXXXX")).trim();
const quote = (value: string) => "'" + 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();
Expand All @@ -650,7 +650,7 @@ export function workflowCode(workflow: WorkflowId, agents: ReturnType<typeof wor
await Promise.all(paths.map((cwd, index) => 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.
Expand Down
Loading
Loading