refactor(automations): streamline model eligibility handling in automation creation

- Removed the eligibility gate for model selection in the automation creation process, allowing users to choose models directly in the builder.
- Updated the `AutomationBuilderForm` to incorporate model selection logic, ensuring that selected models are validated and preserved during automation creation and editing.
- Simplified the `AutomationsContent` and `AutomationNewContent` components by eliminating unnecessary eligibility checks and alerts.
- Enhanced the user experience by integrating model selection directly into the automation approval process, ensuring that only billable models are used.
- Refactored related tests to cover new model selection behavior and ensure proper validation of user-selected models.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-05-29 20:27:40 -07:00
parent fade9d1b9d
commit 9d1a01eb0c
13 changed files with 887 additions and 263 deletions

View file

@ -66,6 +66,19 @@ export const builderExecutionSchema = z.object({
});
export type BuilderExecution = z.infer<typeof builderExecutionSchema>;
/**
* Per-automation model selection. ``0`` means "unset" the builder resolves it
* to the eligible default during render, and the resolved (non-zero) ids are
* written onto ``definition.models`` at submit so the run is insulated from
* later chat/search-space model changes.
*/
export const builderModelsSchema = z.object({
agentLlmId: z.number().int(),
imageConfigId: z.number().int(),
visionConfigId: z.number().int(),
});
export type BuilderModels = z.infer<typeof builderModelsSchema>;
export const builderFormSchema = z.object({
name: z.string().trim().min(1, "Give your automation a name").max(200),
description: z.string().trim().max(2000).nullable(),
@ -77,6 +90,8 @@ export const builderFormSchema = z.object({
tags: z.array(z.string()),
/** Carried through from an edited definition so we don't drop it. */
goal: z.string().nullable(),
/** Selected agent/image/vision models (``0`` = use the eligible default). */
models: builderModelsSchema,
});
export type BuilderForm = z.infer<typeof builderFormSchema>;
@ -132,6 +147,7 @@ export function createEmptyForm(): BuilderForm {
},
tags: [],
goal: null,
models: { agentLlmId: 0, imageConfigId: 0, visionConfigId: 0 },
};
}
@ -218,9 +234,26 @@ function buildDefinition(form: BuilderForm): AutomationDefinition {
on_failure: [],
},
metadata: { tags: form.tags },
// Only emit models when fully resolved (the builder seeds non-zero
// defaults before submit). A zero/unset triple is omitted so the
// backend falls back to the search-space snapshot.
...(hasResolvedModels(form.models)
? {
models: {
agent_llm_id: form.models.agentLlmId,
image_generation_config_id: form.models.imageConfigId,
vision_llm_config_id: form.models.visionConfigId,
},
}
: {}),
} as unknown as AutomationDefinition;
}
/** True once every model slot holds a concrete (non-zero) id. */
export function hasResolvedModels(models: BuilderModels): boolean {
return models.agentLlmId !== 0 && models.imageConfigId !== 0 && models.visionConfigId !== 0;
}
/** The desired schedule trigger for this form, or ``null`` if none. */
export function buildScheduleTrigger(form: BuilderForm): TriggerCreateRequest | null {
if (!form.schedule) return null;
@ -434,6 +467,8 @@ export function hydrateForm(
? metadata.tags.filter((tag): tag is string => typeof tag === "string")
: [];
const models = modelsFromDefinition(d.models);
return {
formable: true,
form: {
@ -455,10 +490,22 @@ export function hydrateForm(
},
tags,
goal: typeof d.goal === "string" ? d.goal : null,
models,
},
};
}
/** Read a captured ``definition.models`` snapshot into the form's model slots. */
function modelsFromDefinition(raw: unknown): BuilderModels {
const m = asRecord(raw);
const num = (value: unknown) => (typeof value === "number" ? value : 0);
return {
agentLlmId: num(m.agent_llm_id),
imageConfigId: num(m.image_generation_config_id),
visionConfigId: num(m.vision_llm_config_id),
};
}
/**
* Project an existing automation into the builder form for editing.
*/