Files
buzz/desktop/src/features/agents/lib/instanceInputForDefinition.ts
T
2026-07-27 13:26:45 +01:00

188 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
AcpRuntime,
AcpRuntimeCatalogEntry,
AgentPersona,
CreateManagedAgentInput,
} from "@/shared/api/types";
import {
getDefaultPersonaRuntime,
resolvePersonaRuntime,
type ResolvePersonaRuntimeResult,
} from "./resolvePersonaRuntime";
import {
getDisabledAcpRuntimeIdsSnapshot,
runtimesForImplicitAcpSelection,
} from "./runtimeVisibilityPreference";
import {
resolveManagedAgentAvatarUrl,
type UploadMediaBytes,
} from "../ui/managedAgentAvatar";
type RuntimesQueryLike = {
isFetched: boolean;
data: readonly AcpRuntimeCatalogEntry[] | undefined;
refetch: () => Promise<{
data?: readonly AcpRuntimeCatalogEntry[] | undefined;
}>;
};
/**
* Acquire the available-runtime list for a start action (Phase 1B.3.5
* row 6). Refetch-aware: an unfetched query is fetched instead of being
* treated as an empty list (which would spuriously refuse every start).
*/
export async function availableRuntimesForStart(
query: RuntimesQueryLike,
): Promise<AcpRuntime[]> {
const entries = query.isFetched ? query.data : (await query.refetch()).data;
return (entries ?? []).filter(
(runtime): runtime is AcpRuntime => runtime.availability === "available",
);
}
/**
* Resolve the runtime a definition should start on, refusing when the
* definition's configured runtime is not available (Phase 1B.3.5 row 1,
* Wes's call: one consistent refuse-with-actionable-error everywhere —
* never silently start on a different runtime than configured).
*/
export function resolveStartRuntimeForDefinition(
persona: AgentPersona,
runtimes: readonly AcpRuntime[],
preferredRuntimeId?: string | null,
disabledRuntimeIds: readonly string[] = getDisabledAcpRuntimeIdsSnapshot(),
): { runtime: AcpRuntime; warnings: string[] } {
const selectableRuntimes = runtimesForImplicitAcpSelection(
runtimes,
disabledRuntimeIds,
persona.runtime,
);
// Use the buzz-agent-first preference (buzz-agent → goose → first available)
// so a freshly installed goose never beats the bundled buzz-agent sidecar
// for runtime-less personas (item 13 regression guard).
const defaultRuntime = getDefaultPersonaRuntime(
selectableRuntimes,
preferredRuntimeId,
);
const { runtime, warnings, isOverridden }: ResolvePersonaRuntimeResult =
resolvePersonaRuntime(persona.runtime, selectableRuntimes, defaultRuntime);
if (!runtime) {
throw new Error("No available runtime found for this agent.");
}
if (isOverridden) {
throw new Error(
warnings[0] ??
"This agent's configured runtime is not available. Install the runtime or edit the agent before starting it.",
);
}
return { runtime, warnings };
}
/**
* Where the started instance should run when the user picked something other
* than plain local in the definition-create flow (B5). Absent intent =
* today's local mapping, byte-identical.
*
* - `provider`: remote backend. Mirrors the legacy provider-mode create:
* no local ACP/agent/MCP commands are spawned, so none are set;
* `startOnAppLaunch` is forced false (remote agents don't auto-start with
* the desktop) and `spawnAfterCreate` true.
* - `mesh`: relay-mesh compute. The preset patch carries the instance
* commands/env the legacy dialog fanned into its field state; env lands in
* record env_vars (the instance-override layer — the dial pointer is
* per-instance runtime state, never definition env). `harnessOverride`
* is true because the preset commands deliberately override the
* definition's runtime preference.
*/
export type BackendIntent = {
type: "provider";
id: string;
config: Record<string, unknown>;
};
/** Keep every definition-start surface aligned on runtime pinning semantics. */
export function shouldPinSelectedRuntimeForDefinition(
definitionRuntimeId: string | null | undefined,
selectedRuntimeId: string,
): boolean {
return (
!definitionRuntimeId || definitionRuntimeId.trim() === selectedRuntimeId
);
}
/**
* The single definition→instance mapping (Phase 1B.3.5 rows 2–4). Every
* surface that creates a running instance from a definition builds its
* CreateManagedAgentInput here so the mapping cannot drift per-site.
*
* - harnessOverride uses the backend-aligned formula: true only when the
* definition has no runtime preference or the picked runtime matches it
* (`create_time_agent_command_override` stores None when picked ==
* inherited; on fallback `harness_override: false` keeps the definition
* authoritative).
* - avatarUrl goes through resolveManagedAgentAvatarUrl (base64 data URIs
* upload via the injectable `upload`; other URLs pass through unchanged).
* - envVars are never seeded from the definition: record.env_vars is
* agent overrides only and spawn merges the live definition env
* underneath. Seeding would manufacture pseudo-overrides that mask
* later definition edits made before the first spawn. (Mesh preset env is
* the deliberate exception: it is instance-override state, not
* definition env.)
*/
export async function buildInstanceInputForDefinition(
persona: AgentPersona,
runtime: AcpRuntime,
upload?: UploadMediaBytes,
backendIntent?: BackendIntent,
): Promise<CreateManagedAgentInput> {
const avatarUrl = await resolveManagedAgentAvatarUrl(
persona.avatarUrl,
upload,
);
const base = {
name: persona.displayName,
personaId: persona.id,
systemPrompt: persona.systemPrompt,
avatarUrl,
};
if (backendIntent?.type === "provider") {
return {
...base,
harnessOverride: false,
spawnAfterCreate: true,
startOnAppLaunch: false,
backend: {
type: "provider",
id: backendIntent.id,
config: backendIntent.config,
},
};
}
return {
...base,
acpCommand: "buzz-acp",
agentCommand: runtime.command,
// Do NOT seed agentArgs from runtime.defaultArgs: record.agent_args must
// remain empty so spawn resolves args live from the definition on every
// start. Seeding here would freeze the args at create-time, silently
// ignoring any later definition-arg edits (Thufir F5 / phase B-5).
// envVars are intentionally never seeded for the same reason (see comment
// at top of this function).
agentArgs: [],
mcpCommand: runtime.mcpCommand ?? "",
harnessOverride: shouldPinSelectedRuntimeForDefinition(
persona.runtime,
runtime.id,
),
model: persona.model ?? undefined,
provider: persona.provider ?? undefined,
spawnAfterCreate: true,
startOnAppLaunch: true,
backend: { type: "local" },
};
}