diff --git a/desktop/src/agent-personas.test.ts b/desktop/src/agent-personas.test.ts index 928ada3..9dbee97 100644 --- a/desktop/src/agent-personas.test.ts +++ b/desktop/src/agent-personas.test.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { afterEach, describe, expect, it } from "vitest"; +import { getAgentSkills } from "./agent-catalog"; import { AGENT_PERSONAS, DEFAULT_AGENT_PERSONAS, @@ -27,6 +28,13 @@ afterEach(() => { } }); +type AgentListEntry = { id: string; skills?: readonly string[] } & Record; + +function agentList(config: { agents?: unknown }): AgentListEntry[] { + const agents = (config.agents ?? {}) as { list?: unknown }; + return (Array.isArray(agents.list) ? agents.list : []) as AgentListEntry[]; +} + describe("agent personas", () => { it("registers only main for a new installation", () => { const stateDir = createStateDir(); @@ -211,4 +219,84 @@ describe("agent personas", () => { ), ).toBe(path.join(openClawHome, "archive")); }); + + it("writes each generated agent's catalog skills into agents.list", () => { + const stateDir = createStateDir(); + const persona = getAgentPersona("master-archive"); + if (!persona) throw new Error("Master Archive persona is not registered"); + const config = { agents: {} }; + + ensureAgentPersonasConfig(config, stateDir, [...DEFAULT_AGENT_PERSONAS, persona]); + + for (const entry of agentList(config)) { + expect(entry.skills).toEqual(getAgentSkills(entry.id)); + } + }); + + it("gives each agent a distinct skills array instance", () => { + const stateDir = createStateDir(); + const persona = getAgentPersona("master-archive"); + if (!persona) throw new Error("Master Archive persona is not registered"); + const config = { agents: {} }; + + ensureAgentPersonasConfig(config, stateDir, [...DEFAULT_AGENT_PERSONAS, persona]); + + const mainEntry = agentList(config).find((entry) => entry.id === "main"); + const archiveEntry = agentList(config).find( + (entry) => entry.id === "master-archive", + ); + if (!mainEntry || !archiveEntry) throw new Error("expected both agents"); + + expect(mainEntry.skills).not.toBe(archiveEntry.skills); + (mainEntry.skills as string[]).push("mutated"); + expect(archiveEntry.skills).not.toContain("mutated"); + expect(getAgentSkills("master-archive")).not.toContain("mutated"); + expect(getAgentSkills("main")).not.toContain("mutated"); + }); + + it("overwrites a stale skills value for a catalog-known agent", () => { + const stateDir = createStateDir(); + const config = { + agents: { + list: [{ id: "main", name: "Assistant", default: true, skills: ["stale-skill"] }], + }, + }; + + const result = ensureAgentPersonasConfig(config, stateDir); + + const mainEntry = agentList(config).find((entry) => entry.id === "main"); + expect(mainEntry?.skills).toEqual(getAgentSkills("main")); + expect(result.changed).toBe(true); + }); + + it("leaves a custom non-catalog agent without an injected skills field", () => { + const stateDir = createStateDir(); + const config = { + agents: { + list: [ + { id: "main", name: "Assistant", default: true }, + { id: "custom-bot", name: "Custom Bot" }, + ], + }, + }; + + ensureAgentPersonasConfig(config, stateDir); + + const customEntry = agentList(config).find((entry) => entry.id === "custom-bot"); + expect(customEntry).not.toHaveProperty("skills"); + const mainEntry = agentList(config).find((entry) => entry.id === "main"); + expect(mainEntry?.skills).toEqual(getAgentSkills("main")); + }); + + it("reports changed when skills are added to a config that lacked them", () => { + const stateDir = createStateDir(); + const config = { + agents: { + list: [{ id: "main", name: "Assistant", default: true }], + }, + }; + + expect(ensureAgentPersonasConfig(config, stateDir).changed).toBe(true); + expect(ensureAgentPersonasConfig(config, stateDir).changed).toBe(false); + }); }); diff --git a/desktop/src/agent-personas.ts b/desktop/src/agent-personas.ts index fd56851..520092f 100644 --- a/desktop/src/agent-personas.ts +++ b/desktop/src/agent-personas.ts @@ -12,6 +12,7 @@ interface WorkspaceFiles { export interface AgentPersona { id: string; name: string; + skills: readonly string[]; workspaceDirName?: string; workspaceFiles?: WorkspaceFiles; } @@ -112,6 +113,7 @@ const PERSONA_PROFILES: Record = { export const AGENT_PERSONAS: readonly AgentPersona[] = AGENT_CATALOG.map((agent) => ({ id: agent.id, name: agent.name, + skills: agent.skills, workspaceDirName: agent.workspaceDirName, workspaceFiles: agent.personaProfile ? PERSONA_PROFILES[agent.personaProfile] : undefined, })); @@ -150,6 +152,7 @@ function createAgentEntry(persona: AgentPersona, stateDir: string): Record = { name: persona.name }; const workspace = getAgentWorkspacePath(stateDir, persona); if (workspace) entry.workspace = workspace; + entry.skills = [...persona.skills]; return entry; } @@ -252,6 +255,16 @@ export function ensureAgentPersonasConfig( } } + const catalogSkillsById = new Map( + AGENT_PERSONAS.map((persona) => [persona.id, persona.skills]), + ); + for (const entry of entries) { + const catalogSkills = catalogSkillsById.get(entry.id); + if (catalogSkills !== undefined) { + entry.skills = [...catalogSkills]; + } + } + const changed = !hasList || Object.hasOwn(agents, "entries") ||