mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix: promote a flow input to the model button only where its control can edit it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0de006f47d
commit
7d56253e26
@@ -24,7 +24,7 @@
|
||||
import { Plug, Plus } from 'lucide-svelte'
|
||||
import { resource } from 'runed'
|
||||
import {
|
||||
composerDrivesEffort,
|
||||
composerDrivenFields,
|
||||
type AgentModelWiring,
|
||||
type ProviderField
|
||||
} from './agentChatInputs'
|
||||
@@ -45,8 +45,10 @@
|
||||
return name ? values[name] : wiring.fixed[field]
|
||||
}
|
||||
|
||||
const driven = $derived(composerDrivenFields(wiring))
|
||||
|
||||
function editable(field: ProviderField): boolean {
|
||||
return wiring.whole !== undefined || wiring.fields[field] !== undefined
|
||||
return driven.has(field)
|
||||
}
|
||||
|
||||
/** Written together, because choosing a resource also invalidates the model. */
|
||||
@@ -63,7 +65,7 @@
|
||||
|
||||
const resourceEditable = $derived(editable('resource'))
|
||||
const modelEditable = $derived(editable('model'))
|
||||
const effortEditable = $derived(composerDrivesEffort(wiring))
|
||||
const effortEditable = $derived(editable('reasoning_effort'))
|
||||
// Wired, but left to the Configure-inputs modal: the button has no model to place it on.
|
||||
const effortInModal = $derived(wiring.fields.reasoning_effort !== undefined && !effortEditable)
|
||||
// Nothing to write: the flow fixes the lot, so the button names it and opens nothing.
|
||||
@@ -180,7 +182,9 @@
|
||||
// arrives async, so there is nothing to carry the current one against — nor the
|
||||
// effort, which only means something against a model. Cleared only where the
|
||||
// registry can speak for the new provider, for the same reason as `effortPatch`.
|
||||
model: undefined,
|
||||
// `''`, not `undefined`: storage drops an undefined key, and the schema's default
|
||||
// model would then come back under the new provider on reload.
|
||||
model: '',
|
||||
...(getReasoningCapability(picked, '').known ? { reasoning_effort: '' } : {})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,6 +144,20 @@ describe('agentModelWiringInputs', () => {
|
||||
expect(agentModelWiringInputs(wiring)).toEqual([])
|
||||
expect(showsModelButton(wiring)).toBe(false)
|
||||
})
|
||||
|
||||
// The same one level up: agents on different providers leave the model menu nothing to
|
||||
// list and no provider to gate a typed id, so the shared model input stays in the modal.
|
||||
it('leaves a shared model to the modal when the agents fix different providers', () => {
|
||||
const wiring = resolveAgentModelWiring([
|
||||
agent(`({ "kind": "openai", "resource": "$res:u/admin/oai", model: flow_input.model })`),
|
||||
agent(
|
||||
`({ "kind": "azure_openai", "resource": "$res:u/admin/azure", model: flow_input.model })`
|
||||
)
|
||||
])
|
||||
expect(wiring?.fields.model).toBe('model')
|
||||
expect(agentModelWiringInputs(wiring)).toEqual([])
|
||||
expect(showsModelButton(wiring)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// The modal is whatever this does not return, so the two cannot disagree about an input.
|
||||
|
||||
@@ -314,38 +314,47 @@ export function agentModelGap(wiring: AgentModelWiring | undefined): string | un
|
||||
}
|
||||
|
||||
/**
|
||||
* The flow inputs the model button actually writes, so the modal does not ask for them a
|
||||
* second time — and, just as much, so it still asks for the ones the button cannot reach.
|
||||
* The provider fields the model button edits. Everything else wired to an input is left to
|
||||
* the Configure-inputs modal, and the button draws no control for it.
|
||||
*
|
||||
* `kind` is the one to watch: the button writes it only alongside a resource, since a
|
||||
* provider is picked as a pair. A flow that wires `kind` to an input while fixing the
|
||||
* resource leaves the button nothing to write it with, and hiding it would leave the run
|
||||
* without a provider kind and no way to supply one.
|
||||
*
|
||||
* `reasoning_effort` is the button's only where it has a model to place the effort against
|
||||
* (see `composerDrivesEffort`).
|
||||
* A field is the button's only where its control can be used, which each field makes depend
|
||||
* on the one before it. An input promoted without a usable control is one nothing can edit.
|
||||
* - `resource` is always usable: the submenu lists the workspace's AI resources.
|
||||
* - `kind` is written only alongside a resource, since a provider is picked as a pair. Wired
|
||||
* with the resource fixed, the button has nothing to write it with.
|
||||
* - `model` needs a provider to list models for and to gate the typed entry: a wired resource
|
||||
* or kind, or one fixed kind. Agents that fix different kinds leave none.
|
||||
* - `reasoning_effort` needs a model to place the effort on: a driven model or one fixed
|
||||
* model. Agents that fix different models leave none.
|
||||
*/
|
||||
export function composerDrivenFields(wiring: AgentModelWiring): Set<ProviderField> {
|
||||
if (wiring.whole) return new Set(PROVIDER_FIELDS)
|
||||
const wired = (field: ProviderField) => wiring.fields[field] !== undefined
|
||||
const driven = new Set<ProviderField>()
|
||||
if (wired('resource')) {
|
||||
driven.add('resource')
|
||||
if (wired('kind')) driven.add('kind')
|
||||
}
|
||||
const providerKnown = wired('resource') || wired('kind') || fixedOne(wiring, 'kind')
|
||||
if (wired('model') && providerKnown) driven.add('model')
|
||||
const modelKnown = driven.has('model') || fixedOne(wiring, 'model')
|
||||
if (wired('reasoning_effort') && modelKnown) driven.add('reasoning_effort')
|
||||
return driven
|
||||
}
|
||||
|
||||
/** Whether every agent fixes the field to the same non-empty literal. */
|
||||
function fixedOne(wiring: AgentModelWiring, field: ProviderField): boolean {
|
||||
const value = wiring.fixed[field]
|
||||
return value !== undefined && value !== ''
|
||||
}
|
||||
|
||||
/** The flow inputs the model button writes, so the modal does not ask for them twice. */
|
||||
export function agentModelWiringInputs(wiring: AgentModelWiring | undefined): string[] {
|
||||
if (!wiring) return []
|
||||
if (wiring.whole) return [wiring.whole]
|
||||
const driven: ProviderField[] = ['resource', 'model']
|
||||
if (wiring.fields.resource !== undefined) driven.push('kind')
|
||||
if (composerDrivesEffort(wiring)) driven.push('reasoning_effort')
|
||||
return driven.map((field) => wiring.fields[field]).filter((name): name is string => !!name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the model button draws the thinking control for a wired effort.
|
||||
*
|
||||
* Every thinking state but one is usable: a ladder, a typed token, or why the model has
|
||||
* neither. The exception is a model the button cannot name, as when agents share an effort
|
||||
* input but fix different models. It would say "Pick a model first" with no model to pick,
|
||||
* so the effort stays with the modal.
|
||||
*/
|
||||
export function composerDrivesEffort(wiring: AgentModelWiring): boolean {
|
||||
if (wiring.whole) return true
|
||||
if (wiring.fields.reasoning_effort === undefined) return false
|
||||
return wiring.fields.model !== undefined || !agentFieldEmpty(wiring, 'model')
|
||||
return [...composerDrivenFields(wiring)]
|
||||
.map((field) => wiring.fields[field])
|
||||
.filter((name): name is string => !!name)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,7 +363,7 @@ export function composerDrivesEffort(wiring: AgentModelWiring): boolean {
|
||||
*/
|
||||
export function showsModelButton(wiring: AgentModelWiring | undefined): boolean {
|
||||
if (!wiring) return false
|
||||
return agentModelWiringInputs(wiring).length > 0 || !agentFieldEmpty(wiring, 'model')
|
||||
return agentModelWiringInputs(wiring).length > 0 || fixedOne(wiring, 'model')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user