feat: provider/model selection for personas and runtime-aware env injection (#794)

This commit is contained in:
Will Pfleger
2026-06-09 16:42:27 -04:00
committed by GitHub
parent 762a45969a
commit 9a98e60fc2
26 changed files with 1135 additions and 76 deletions

View File

@@ -66,6 +66,7 @@ export function PersonaDialog({
const [systemPrompt, setSystemPrompt] = React.useState("");
const [runtime, setRuntime] = React.useState("");
const [model, setModel] = React.useState("");
const [provider, setProvider] = React.useState("");
const [namePoolText, setNamePoolText] = React.useState("");
const [envVars, setEnvVars] = React.useState<EnvVarsValue>({});
const [isImportingUpdate, setIsImportingUpdate] = React.useState(false);
@@ -90,6 +91,7 @@ export function PersonaDialog({
setSystemPrompt(initialValues.systemPrompt);
setRuntime(initialValues.runtime ?? "");
setModel(initialValues.model ?? "");
setProvider(initialValues.provider ?? "");
setNamePoolText(
("namePool" in initialValues
? (initialValues as { namePool?: string[] }).namePool
@@ -216,6 +218,7 @@ export function PersonaDialog({
setSystemPrompt("");
setRuntime("");
setModel("");
setProvider("");
setNamePoolText("");
setImportErrorMessage(null);
setIsImportingUpdate(false);
@@ -240,6 +243,7 @@ export function PersonaDialog({
systemPrompt,
runtime: runtime.trim() || undefined,
model: model.trim() || undefined,
provider: provider.trim() || undefined,
namePool: namePool.length > 0 ? namePool : undefined,
envVars,
};
@@ -405,6 +409,27 @@ export function PersonaDialog({
</p>
</div>
<div className="space-y-1.5">
<label className="text-sm font-medium" htmlFor="persona-provider">
LLM Provider
</label>
<Input
autoCapitalize="none"
autoCorrect="off"
disabled={isPending}
id="persona-provider"
onChange={(event) => setProvider(event.target.value)}
placeholder="e.g. databricks, anthropic, openai"
spellCheck={false}
value={provider}
/>
<p className="text-xs text-muted-foreground">
Optional. Injected as the runtime's provider env var at agent
creation time. Leave blank for auto-detection or provider-locked
runtimes.
</p>
</div>
<div className="space-y-1.5">
<label
className="text-sm font-medium"

View File

@@ -31,6 +31,7 @@ test("duplicatePersonaDialogState copies persona fields into a new draft", () =>
systemPrompt: "Be direct.",
runtime: "provider-a",
model: "model-a",
provider: null,
isBuiltIn: false,
isActive: true,
createdAt: "2025-01-01T00:00:00Z",
@@ -43,6 +44,7 @@ test("duplicatePersonaDialogState copies persona fields into a new draft", () =>
systemPrompt: "Be direct.",
runtime: "provider-a",
model: "model-a",
provider: undefined,
namePool: [],
envVars: {},
});
@@ -82,6 +84,7 @@ test("editPersonaDialogState preserves the persona id for updates", () => {
systemPrompt: "Keep it weird.",
runtime: null,
model: null,
provider: null,
isBuiltIn: true,
isActive: true,
createdAt: "2025-01-01T00:00:00Z",
@@ -98,6 +101,7 @@ test("editPersonaDialogState preserves the persona id for updates", () => {
systemPrompt: "Keep it weird.",
runtime: undefined,
model: undefined,
provider: undefined,
namePool: [],
envVars: {},
});
@@ -132,6 +136,8 @@ test("importPersonaDialogState maps parsed persona previews into create drafts",
systemPrompt: "Imported prompt",
runtime: null,
model: "model-b",
provider: null,
namePool: [],
sourceFile: "import.persona.json",
});
@@ -142,5 +148,83 @@ test("importPersonaDialogState maps parsed persona previews into create drafts",
systemPrompt: "Imported prompt",
runtime: undefined,
model: "model-b",
provider: undefined,
});
});
// ── Provider round-trip tests ─────────────────────────────────────────────────
test("editPersonaDialogState preserves provider=databricks", () => {
const state = editPersonaDialogState({
id: "persona-provider",
displayName: "DB Agent",
avatarUrl: null,
systemPrompt: "Use databricks.",
runtime: "goose",
model: "dbrx",
provider: "databricks",
isBuiltIn: false,
isActive: true,
namePool: [],
envVars: {},
createdAt: "2025-01-01T00:00:00Z",
updatedAt: "2025-01-02T00:00:00Z",
});
assert.equal(state.initialValues.provider, "databricks");
});
test("editPersonaDialogState maps provider=null to undefined", () => {
const state = editPersonaDialogState({
id: "persona-no-provider",
displayName: "Plain",
avatarUrl: null,
systemPrompt: "No provider.",
runtime: null,
model: null,
provider: null,
isBuiltIn: false,
isActive: true,
namePool: [],
envVars: {},
createdAt: "2025-01-01T00:00:00Z",
updatedAt: "2025-01-02T00:00:00Z",
});
assert.equal(state.initialValues.provider, undefined);
});
test("duplicatePersonaDialogState preserves provider=databricks", () => {
const state = duplicatePersonaDialogState({
id: "persona-dup-provider",
displayName: "DB Agent",
avatarUrl: null,
systemPrompt: "Use databricks.",
runtime: "goose",
model: "dbrx",
provider: "databricks",
isBuiltIn: false,
isActive: true,
namePool: [],
envVars: {},
createdAt: "2025-01-01T00:00:00Z",
updatedAt: "2025-01-02T00:00:00Z",
});
assert.equal(state.initialValues.provider, "databricks");
});
test("importPersonaDialogState preserves provider=anthropic", () => {
const state = importPersonaDialogState({
displayName: "Imported With Provider",
avatarDataUrl: null,
systemPrompt: "Anthropic agent.",
runtime: "goose",
model: "claude-sonnet",
provider: "anthropic",
namePool: [],
sourceFile: "provider-test.persona.json",
});
assert.equal(state.initialValues.provider, "anthropic");
});

View File

@@ -44,6 +44,7 @@ export function duplicatePersonaDialogState(
systemPrompt: persona.systemPrompt,
runtime: persona.runtime ?? undefined,
model: persona.model ?? undefined,
provider: persona.provider ?? undefined,
// Carry envVars and namePool into the duplicate. Without this, a
// duplicated persona that relies on an API key in env_vars would
// silently fail at spawn until the user re-entered every credential.
@@ -69,6 +70,7 @@ export function editPersonaDialogState(
systemPrompt: persona.systemPrompt,
runtime: persona.runtime ?? undefined,
model: persona.model ?? undefined,
provider: persona.provider ?? undefined,
// Seed both namePool and envVars from the loaded persona so editing
// unrelated fields doesn't submit an empty value that wipes them.
// (Persona update treats Some(empty) as "clear all" intentionally;
@@ -92,6 +94,7 @@ export function importPersonaDialogState(
systemPrompt: persona.systemPrompt,
runtime: persona.runtime ?? undefined,
model: persona.model ?? undefined,
provider: persona.provider ?? undefined,
},
};
}

View File

@@ -12,6 +12,7 @@ type RawParsedPersonaPreview = {
avatar_data_url: string | null;
runtime: string | null;
model: string | null;
provider: string | null;
name_pool?: string[];
source_file: string;
};
@@ -33,6 +34,7 @@ export type ParsedPersonaPreview = {
avatarDataUrl: string | null;
runtime: string | null;
model: string | null;
provider: string | null;
namePool: string[];
sourceFile: string;
};
@@ -54,6 +56,7 @@ type RawPersona = {
system_prompt: string;
runtime?: string | null;
model?: string | null;
provider?: string | null;
name_pool?: string[];
is_builtin: boolean;
is_active?: boolean;
@@ -70,6 +73,7 @@ function fromRawPersona(persona: RawPersona): AgentPersona {
systemPrompt: persona.system_prompt,
runtime: persona.runtime ?? null,
model: persona.model ?? null,
provider: persona.provider ?? null,
namePool: persona.name_pool ?? [],
isBuiltIn: persona.is_builtin,
isActive: persona.is_active ?? true,
@@ -94,6 +98,7 @@ export async function createPersona(
systemPrompt: input.systemPrompt,
runtime: input.runtime,
model: input.model,
provider: input.provider,
namePool: input.namePool ?? [],
envVars: input.envVars ?? {},
},
@@ -113,6 +118,7 @@ export async function updatePersona(
systemPrompt: input.systemPrompt,
runtime: input.runtime,
model: input.model,
provider: input.provider,
namePool: input.namePool ?? [],
// Send envVars only when caller explicitly provided it; omitting
// tells the backend "don't touch the stored env vars" so editing
@@ -151,6 +157,7 @@ export async function parsePersonaFiles(
avatarDataUrl: p.avatar_data_url,
runtime: p.runtime,
model: p.model,
provider: p.provider,
namePool: p.name_pool ?? [],
sourceFile: p.source_file,
})),

View File

@@ -469,6 +469,8 @@ export type AgentPersona = {
runtime: string | null;
/** Opaque, harness-specific model identifier string. Sprout stores and passes through without interpretation. */
model: string | null;
/** LLM inference provider (e.g. "databricks", "anthropic"). Injected as the runtime's provider env var at spawn time. */
provider: string | null;
namePool: string[];
isBuiltIn: boolean;
isActive: boolean;
@@ -487,6 +489,7 @@ export type CreatePersonaInput = {
systemPrompt: string;
runtime?: string;
model?: string;
provider?: string;
namePool?: string[];
envVars?: Record<string, string>;
};
@@ -498,6 +501,7 @@ export type UpdatePersonaInput = {
systemPrompt: string;
runtime?: string;
model?: string;
provider?: string;
namePool?: string[];
envVars?: Record<string, string>;
};