mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
322 lines
9.9 KiB
TypeScript
322 lines
9.9 KiB
TypeScript
import {
|
|
buildInstanceInputForDefinition,
|
|
resolveStartRuntimeForDefinition,
|
|
} from "@/features/agents/lib/instanceInputForDefinition";
|
|
import {
|
|
addChannelMembers,
|
|
createManagedAgent,
|
|
discoverAcpRuntimes,
|
|
getChannelMembers,
|
|
listManagedAgents,
|
|
updateManagedAgent,
|
|
} from "@/shared/api/tauri";
|
|
import { getGlobalAgentConfig } from "@/shared/api/tauriGlobalAgentConfig";
|
|
import { listPersonas, setPersonaActive } from "@/shared/api/tauriPersonas";
|
|
import type {
|
|
AcpRuntime,
|
|
AgentPersona,
|
|
CreateManagedAgentInput,
|
|
ManagedAgent,
|
|
} from "@/shared/api/types";
|
|
import { normalizePubkey } from "@/shared/lib/pubkey";
|
|
|
|
export const WELCOME_GUIDE_AGENT_NAME = "Fizz";
|
|
export const WELCOME_GUIDE_PERSONA_ID = "builtin:fizz";
|
|
export const WELCOME_TEAM_ID = "builtin-team:welcome";
|
|
export const WELCOME_GUIDE_INTRO_MARKER = "buzz-welcome-intro.v1";
|
|
const LEGACY_WELCOME_GUIDE_AGENT_NAME = "Kit";
|
|
export const LEGACY_WELCOME_GUIDE_SYSTEM_PROMPT =
|
|
"You are Kit, Sprout's friendly welcome guide. Help new users understand the community, channels, messages, and agents. Keep introductions concise, practical, and warm.";
|
|
export const WELCOME_GUIDE_INTRO_MESSAGE =
|
|
"Hi, I'm Fizz. Welcome to Buzz.\n\nI can help you get oriented, answer questions, and make the first few steps feel less mysterious.\n\nFeel free to ask me what else you can do in Buzz, or just talk through what you want to build.";
|
|
|
|
export type WelcomeTeamRole = "lead" | "teammate";
|
|
|
|
export type WelcomeTeamStarterDefinition = Readonly<{
|
|
name: string;
|
|
personaId: string;
|
|
role: WelcomeTeamRole;
|
|
}>;
|
|
|
|
/** Stable identities used to provision the Rust-seeded Welcome Team. */
|
|
export const WELCOME_TEAM_STARTERS = [
|
|
{ name: "Fizz", personaId: "builtin:fizz", role: "lead" },
|
|
{ name: "Honey", personaId: "builtin:honey", role: "teammate" },
|
|
{ name: "Bumble", personaId: "builtin:bumble", role: "teammate" },
|
|
] as const satisfies readonly WelcomeTeamStarterDefinition[];
|
|
|
|
export type WelcomeTeamAgents = [ManagedAgent, ManagedAgent, ManagedAgent];
|
|
|
|
const welcomeTeamPromises = new Map<string, Promise<WelcomeTeamAgents>>();
|
|
|
|
function normalizeRelayUrl(relayUrl: string | null | undefined) {
|
|
return relayUrl?.trim().replace(/\/+$/, "") ?? null;
|
|
}
|
|
|
|
function isAgentScopedToRelay(agent: ManagedAgent, relayUrl?: string | null) {
|
|
const targetRelayUrl = normalizeRelayUrl(relayUrl);
|
|
if (!targetRelayUrl) {
|
|
return true;
|
|
}
|
|
return normalizeRelayUrl(agent.relayUrl) === targetRelayUrl;
|
|
}
|
|
|
|
function isBuiltInWelcomeGuideAgent(agent: ManagedAgent) {
|
|
return agent.personaId === WELCOME_GUIDE_PERSONA_ID;
|
|
}
|
|
|
|
function isLegacyKitWelcomeGuideAgent(agent: ManagedAgent) {
|
|
return (
|
|
agent.name.trim().toLowerCase() ===
|
|
LEGACY_WELCOME_GUIDE_AGENT_NAME.toLowerCase() &&
|
|
agent.systemPrompt?.trim() === LEGACY_WELCOME_GUIDE_SYSTEM_PROMPT
|
|
);
|
|
}
|
|
|
|
function isWelcomeGuideAgent(agent: ManagedAgent) {
|
|
return (
|
|
isBuiltInWelcomeGuideAgent(agent) || isLegacyKitWelcomeGuideAgent(agent)
|
|
);
|
|
}
|
|
|
|
function pickAgentByStatus(agents: ManagedAgent[]) {
|
|
return (
|
|
agents.find((agent) => agent.status === "running") ??
|
|
agents.find((agent) => agent.status === "deployed") ??
|
|
agents[0] ??
|
|
null
|
|
);
|
|
}
|
|
|
|
export function pickWelcomeGuideAgent(agents: ManagedAgent[]) {
|
|
return pickAgentByStatus(agents.filter(isWelcomeGuideAgent));
|
|
}
|
|
|
|
export function pickWelcomeGuideAgentForRelay(
|
|
agents: ManagedAgent[],
|
|
relayUrl?: string | null,
|
|
) {
|
|
return pickAgentByStatus(
|
|
agents.filter(
|
|
(agent) =>
|
|
isWelcomeGuideAgent(agent) && isAgentScopedToRelay(agent, relayUrl),
|
|
),
|
|
);
|
|
}
|
|
|
|
/** Find the preferred managed instance for one starter persona and relay. */
|
|
export function pickWelcomeTeamStarterAgentForRelay(
|
|
agents: ManagedAgent[],
|
|
starter: WelcomeTeamStarterDefinition,
|
|
relayUrl?: string | null,
|
|
) {
|
|
return pickAgentByStatus(
|
|
agents.filter(
|
|
(agent) =>
|
|
agent.teamId === WELCOME_TEAM_ID &&
|
|
agent.personaId === starter.personaId &&
|
|
isAgentScopedToRelay(agent, relayUrl),
|
|
),
|
|
);
|
|
}
|
|
|
|
/** Pubkeys belonging to any managed Welcome Team persona on this relay. */
|
|
export async function getWelcomeTeamAgentPubkeys(relayUrl?: string | null) {
|
|
const personaIds = new Set<string>(
|
|
WELCOME_TEAM_STARTERS.map(({ personaId }) => personaId),
|
|
);
|
|
return (await listManagedAgents())
|
|
.filter(
|
|
(agent) =>
|
|
agent.teamId === WELCOME_TEAM_ID &&
|
|
agent.personaId !== null &&
|
|
personaIds.has(agent.personaId) &&
|
|
isAgentScopedToRelay(agent, relayUrl),
|
|
)
|
|
.map((agent) => agent.pubkey);
|
|
}
|
|
|
|
/** Legacy Fizz/Kit lookup retained for existing channel reuse checks. */
|
|
export async function getWelcomeGuideAgentPubkeys(relayUrl?: string | null) {
|
|
return (await listManagedAgents())
|
|
.filter(
|
|
(agent) =>
|
|
isWelcomeGuideAgent(agent) && isAgentScopedToRelay(agent, relayUrl),
|
|
)
|
|
.map((agent) => agent.pubkey);
|
|
}
|
|
|
|
export async function activateWelcomeTeamPersonasSequentially(
|
|
inactivePersonaIds: readonly string[],
|
|
activate: (personaId: string) => Promise<unknown>,
|
|
) {
|
|
for (const personaId of inactivePersonaIds) {
|
|
await activate(personaId);
|
|
}
|
|
}
|
|
|
|
async function ensureWelcomeTeamPersonasActive() {
|
|
const personas = await listPersonas();
|
|
const personasById = new Map(
|
|
personas.map((persona) => [persona.id, persona]),
|
|
);
|
|
|
|
for (const starter of WELCOME_TEAM_STARTERS) {
|
|
if (!personasById.has(starter.personaId)) {
|
|
throw new Error(`${starter.name} agent not found.`);
|
|
}
|
|
}
|
|
|
|
// Persona activation is a read-modify-write operation over one shared file.
|
|
// Run these sequentially so concurrent writes cannot lose a teammate's
|
|
// activation and leave Welcome provisioning permanently partial.
|
|
await activateWelcomeTeamPersonasSequentially(
|
|
WELCOME_TEAM_STARTERS.filter(
|
|
({ personaId }) => !personasById.get(personaId)?.isActive,
|
|
).map(({ personaId }) => personaId),
|
|
(personaId) => setPersonaActive(personaId, true),
|
|
);
|
|
}
|
|
|
|
async function ensureWelcomeTeamMembership(
|
|
channelId: string,
|
|
agents: WelcomeTeamAgents,
|
|
) {
|
|
const members = await getChannelMembers(channelId).catch(() => []);
|
|
const memberPubkeys = new Set(
|
|
members.map((member) => normalizePubkey(member.pubkey)),
|
|
);
|
|
const missingAgents = agents.filter(
|
|
(agent) => !memberPubkeys.has(normalizePubkey(agent.pubkey)),
|
|
);
|
|
if (missingAgents.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const result = await addChannelMembers({
|
|
channelId,
|
|
pubkeys: missingAgents.map((agent) => agent.pubkey),
|
|
role: "bot",
|
|
});
|
|
const unexpectedError = result.errors.find(
|
|
({ error }) => !error.toLowerCase().includes("already"),
|
|
);
|
|
if (unexpectedError) {
|
|
throw new Error(unexpectedError.error);
|
|
}
|
|
}
|
|
|
|
export async function buildWelcomeStarterCreateInput(
|
|
starter: WelcomeTeamStarterDefinition,
|
|
persona: AgentPersona,
|
|
runtimes: readonly AcpRuntime[],
|
|
preferredRuntimeId: string | null,
|
|
relayUrl?: string | null,
|
|
): Promise<CreateManagedAgentInput> {
|
|
const { runtime } = resolveStartRuntimeForDefinition(
|
|
persona,
|
|
runtimes,
|
|
preferredRuntimeId,
|
|
);
|
|
return {
|
|
...(await buildInstanceInputForDefinition(persona, runtime)),
|
|
name: starter.name,
|
|
teamId: WELCOME_TEAM_ID,
|
|
relayUrl: relayUrl ?? undefined,
|
|
spawnAfterCreate: false,
|
|
startOnAppLaunch: false,
|
|
respondTo: "owner-only",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Ensure the complete built-in Welcome Team is ready for kickoff.
|
|
* The team itself is Rust-seeded; this only activates personas, creates any
|
|
* missing relay-scoped instances, and adds all three to Welcome as bots.
|
|
*/
|
|
async function provisionWelcomeTeam(
|
|
channelId: string,
|
|
relayUrl?: string | null,
|
|
): Promise<WelcomeTeamAgents> {
|
|
const existingAgents = await listManagedAgents();
|
|
await ensureWelcomeTeamPersonasActive();
|
|
const [personas, runtimeCatalog, globalConfig] = await Promise.all([
|
|
listPersonas(),
|
|
discoverAcpRuntimes(),
|
|
getGlobalAgentConfig(),
|
|
]);
|
|
const personasById = new Map(
|
|
personas.map((persona) => [persona.id, persona]),
|
|
);
|
|
const runtimes = runtimeCatalog.filter(
|
|
(runtime): runtime is AcpRuntime => runtime.availability === "available",
|
|
);
|
|
|
|
const agents: ManagedAgent[] = [];
|
|
for (const starter of WELCOME_TEAM_STARTERS) {
|
|
const existing = pickWelcomeTeamStarterAgentForRelay(
|
|
existingAgents,
|
|
starter,
|
|
relayUrl,
|
|
);
|
|
if (existing) {
|
|
agents.push(existing);
|
|
continue;
|
|
}
|
|
|
|
const persona = personasById.get(starter.personaId);
|
|
if (!persona) {
|
|
throw new Error(`${starter.name} agent not found.`);
|
|
}
|
|
const created = await createManagedAgent(
|
|
await buildWelcomeStarterCreateInput(
|
|
starter,
|
|
persona,
|
|
runtimes,
|
|
globalConfig.preferred_runtime,
|
|
relayUrl,
|
|
),
|
|
);
|
|
agents.push(created.agent);
|
|
}
|
|
const [lead, honey, bumble] = agents;
|
|
if (!lead || !honey || !bumble) {
|
|
throw new Error("Welcome Team provisioning did not return every starter.");
|
|
}
|
|
const welcomeAgents: WelcomeTeamAgents = [lead, honey, bumble];
|
|
const leadPubkey = lead.pubkey;
|
|
for (const index of [1, 2] as const) {
|
|
const teammate = welcomeAgents[index];
|
|
const alreadyAllowsLead =
|
|
teammate.respondTo === "allowlist" &&
|
|
teammate.respondToAllowlist.some(
|
|
(pubkey) => normalizePubkey(pubkey) === normalizePubkey(leadPubkey),
|
|
);
|
|
if (!alreadyAllowsLead) {
|
|
const updated = await updateManagedAgent({
|
|
pubkey: teammate.pubkey,
|
|
respondTo: "allowlist",
|
|
respondToAllowlist: [leadPubkey],
|
|
});
|
|
welcomeAgents[index] = updated.agent;
|
|
}
|
|
}
|
|
await ensureWelcomeTeamMembership(channelId, welcomeAgents);
|
|
return welcomeAgents;
|
|
}
|
|
|
|
export function ensureWelcomeTeam(
|
|
channelId: string,
|
|
relayUrl?: string | null,
|
|
): Promise<WelcomeTeamAgents> {
|
|
const key = `${normalizeRelayUrl(relayUrl) ?? ""}:${channelId}`;
|
|
const current = welcomeTeamPromises.get(key);
|
|
if (current) return current;
|
|
|
|
const promise = provisionWelcomeTeam(channelId, relayUrl).finally(() =>
|
|
welcomeTeamPromises.delete(key),
|
|
);
|
|
welcomeTeamPromises.set(key, promise);
|
|
return promise;
|
|
}
|