Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions desktop/src/agent-personas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,6 +28,13 @@ afterEach(() => {
}
});

type AgentListEntry = { id: string; skills?: readonly string[] } & Record<string, unknown>;

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();
Expand Down Expand Up @@ -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);
});
});
13 changes: 13 additions & 0 deletions desktop/src/agent-personas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface WorkspaceFiles {
export interface AgentPersona {
id: string;
name: string;
skills: readonly string[];
workspaceDirName?: string;
workspaceFiles?: WorkspaceFiles;
}
Expand Down Expand Up @@ -112,6 +113,7 @@ const PERSONA_PROFILES: Record<PersonaProfile, WorkspaceFiles> = {
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,
}));
Expand Down Expand Up @@ -150,6 +152,7 @@ function createAgentEntry(persona: AgentPersona, stateDir: string): Record<strin
const entry: Record<string, unknown> = { name: persona.name };
const workspace = getAgentWorkspacePath(stateDir, persona);
if (workspace) entry.workspace = workspace;
entry.skills = [...persona.skills];
return entry;
}

Expand Down Expand Up @@ -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") ||
Expand Down