fix(agents): carry the backend provider into persona templates

Corrects an earlier call to omit provider from the save-as-persona-template
port. On main the provider is NOT a top-level ManagedAgent field, but it is
NOT absent either — it lives in the backend union: a `{ type: "provider",
id }` backend carries the provider id, while a `{ type: "local" }` backend
has none.

- saveAsPersonaTemplateDialogState now derives provider from agent.backend
  (providerForAgentBackend): a provider backend carries backend.id; a local
  backend leaves it unset. A databricks agent promotes with its provider
  instead of losing it.
- CreateAgentDialog's opt-in template mint mirrors this: it carries the
  selected backend provider (runOn) when the agent runs on a provider.

Tests updated: provider carries from a provider backend; stays unset for a
local backend.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
2026-06-26 13:59:38 -07:00
co-authored by Taylor Ho
parent 65e41bbc48
commit b6519f5f0e
3 changed files with 46 additions and 11 deletions
@@ -443,6 +443,10 @@ export function CreateAgentDialog({
runtime:
selectedRuntimeId !== "custom" ? selectedRuntimeId : undefined,
model: useMesh ? meshModelId.trim() || undefined : undefined,
// Carry the backend provider when the agent runs on one ("Run on" is
// a provider, not local). Mirrors what the agent input persists in
// `backend.id`; local agents leave it unset.
provider: isProviderMode ? runOn : undefined,
envVars,
};
try {
@@ -259,6 +259,7 @@ function makeAgent(overrides = {}) {
lastStoppedAt: null,
lastExitCode: null,
lastError: null,
backend: { type: "local" },
...overrides,
};
}
@@ -316,12 +317,26 @@ test("saveAsPersonaTemplateDialogState carries name, prompt, model, and envVars"
assert.deepEqual(state.initialValues.namePool, []);
});
test("saveAsPersonaTemplateDialogState omits provider (no top-level field on ManagedAgent)", () => {
// A ManagedAgent has no top-level provider, so there is nothing lossless to
// carry — the builder must not invent one.
const state = saveAsPersonaTemplateDialogState(makeAgent(), []);
test("saveAsPersonaTemplateDialogState carries the provider id from a provider backend", () => {
// A databricks/anthropic agent must promote with its provider, not lose it.
// ManagedAgent has no top-level provider on main; it lives in backend.id.
const state = saveAsPersonaTemplateDialogState(
makeAgent({ backend: { type: "provider", id: "databricks", config: {} } }),
[],
);
assert.equal("provider" in state.initialValues, false);
assert.equal(state.initialValues.provider, "databricks");
});
test("saveAsPersonaTemplateDialogState leaves provider unset for a local backend", () => {
// A local backend has no provider; the persona's provider is optional, so it
// carries as undefined (auto-detect / provider-locked runtime).
const state = saveAsPersonaTemplateDialogState(
makeAgent({ backend: { type: "local" } }),
[],
);
assert.equal(state.initialValues.provider, undefined);
});
test("saveAsPersonaTemplateDialogState tolerates null systemPrompt and model", () => {
@@ -76,21 +76,34 @@ function runtimeIdForAgentCommand(
return match?.id;
}
/**
* Extract the LLM provider id from a managed agent's backend so it can carry
* into the persona template (a databricks/anthropic agent should promote with
* its provider, not lose it). On `main` the provider is NOT a top-level field
* on `ManagedAgent`; it lives in the backend union: a `"provider"` backend
* carries the provider id, while a `"local"` backend has none. Returns
* `undefined` for local backends — the persona's `provider` is optional, so an
* absent provider just carries as unset (auto-detect / provider-locked runtime).
*/
function providerForAgentBackend(
backend: ManagedAgent["backend"],
): string | undefined {
return backend.type === "provider" ? backend.id : undefined;
}
/**
* Dialog state for the opt-in "Save as persona template" action on an existing
* agent. Prefills the persona editor from the agent so the user reviews and
* confirms before a persona template is created — nothing is minted silently.
*
* Near-lossless promote: name, system prompt, model, and env vars copy
* straight across; the harness command reverse-maps to a runtime ID.
* `namePool` is persona-only and starts empty — the user can fill it in the
* same dialog (it's how a template bulk-adds bots later).
* straight across; the harness command reverse-maps to a runtime ID and the
* backend provider id carries into the persona's provider. `namePool` is
* persona-only and starts empty — the user can fill it in the same dialog
* (it's how a template bulk-adds bots later).
*
* Note: "persona template" is the UI name for what the backend calls a
* `persona` (kind:30175). This builder produces a backend `CreatePersonaInput`.
* `provider` is intentionally not carried: a `ManagedAgent` does not expose a
* top-level provider field (it lives in backend/provider config), so there is
* nothing lossless to copy here.
*/
export function saveAsPersonaTemplateDialogState(
agent: ManagedAgent,
@@ -106,6 +119,9 @@ export function saveAsPersonaTemplateDialogState(
systemPrompt: agent.systemPrompt ?? "",
runtime: runtimeIdForAgentCommand(agent.agentCommand, runtimes),
model: agent.model ?? undefined,
// Carry the provider from a `"provider"` backend; `"local"` backends
// have none and leave it unset (see providerForAgentBackend).
provider: providerForAgentBackend(agent.backend),
// namePool is persona-only; start empty so the user fills it here.
namePool: [],
envVars: agent.envVars ?? {},