feat(FN-1171): add agent soul memory and employees workflows

- Add soul and memory fields to agent types and AgentStore with persistence/update test coverage
- Add dashboard routes and API helpers to fetch and update agent soul/memory data
- Extend AgentDetailView with Soul, Memory, and Employees tabs and rename children labels to employees
- Normalize employee route params for type safety and add focused route/component tests for the new flows
This commit is contained in:
gsxdsm
2026-04-08 13:00:12 -07:00
parent ac4d8a18bc
commit 9cb4d3f323
10 changed files with 770 additions and 28 deletions

View File

@@ -81,6 +81,22 @@ describe("AgentStore", () => {
expect(agent.metadata).toEqual({ version: 2, tags: ["test"] });
});
it("persists soul and memory fields on create", async () => {
const agent = await store.createAgent({
name: "With Soul",
role: "executor",
soul: "Calm and precise.",
memory: "Prefers concise code examples.",
});
expect(agent.soul).toBe("Calm and precise.");
expect(agent.memory).toBe("Prefers concise code examples.");
const persisted = await store.getAgent(agent.id);
expect(persisted?.soul).toBe("Calm and precise.");
expect(persisted?.memory).toBe("Prefers concise code examples.");
});
it("throws when name is empty", async () => {
await expect(
store.createAgent({ name: "", role: "executor" })
@@ -540,6 +556,41 @@ describe("AgentStore", () => {
expect(updated.metadata).toEqual({ preserved: true }); // preserved
});
it("updates soul and memory fields", async () => {
const created = await store.createAgent({
name: "Knowledge Agent",
role: "executor",
});
const updated = await store.updateAgent(created.id, {
soul: "Collaborative, practical mentor",
memory: "Avoids broad rewrites; prefers incremental changes.",
});
expect(updated.soul).toBe("Collaborative, practical mentor");
expect(updated.memory).toBe("Avoids broad rewrites; prefers incremental changes.");
const persisted = await store.getAgent(created.id);
expect(persisted?.soul).toBe("Collaborative, practical mentor");
expect(persisted?.memory).toBe("Avoids broad rewrites; prefers incremental changes.");
});
it("does not clear soul when updates.soul is undefined", async () => {
const created = await store.createAgent({
name: "Stable Soul",
role: "executor",
soul: "Patient reviewer",
});
const updated = await store.updateAgent(created.id, {
soul: undefined,
memory: "Remembers coding preferences",
});
expect(updated.soul).toBe("Patient reviewer");
expect(updated.memory).toBe("Remembers coding preferences");
});
it("allows clearing optional fields via explicit undefined", async () => {
const created = await store.createAgent({
name: "Clearable",
@@ -619,7 +670,7 @@ describe("AgentStore", () => {
expect(revisions[0].after.name).toBe("Renamed");
});
it("records revisions for runtimeConfig, permissions, instructionsPath, and instructionsText changes", async () => {
it("records revisions for runtimeConfig, permissions, instructions, soul, and memory changes", async () => {
const created = await store.createAgent({
name: "Configurable",
role: "executor",
@@ -631,6 +682,8 @@ describe("AgentStore", () => {
await store.updateAgent(created.id, { permissions: { canReview: true, canExecute: true } });
await store.updateAgent(created.id, { instructionsPath: "docs/agent.md" });
await store.updateAgent(created.id, { instructionsText: "Follow safety checks." });
await store.updateAgent(created.id, { soul: "Thoughtful collaborator" });
await store.updateAgent(created.id, { memory: "Knows the repository architecture" });
const revisions = await store.getConfigRevisions(created.id);
const changedFields = revisions.flatMap((revision) => revision.diffs.map((diff) => diff.field));
@@ -639,6 +692,8 @@ describe("AgentStore", () => {
expect(changedFields).toContain("permissions");
expect(changedFields).toContain("instructionsPath");
expect(changedFields).toContain("instructionsText");
expect(changedFields).toContain("soul");
expect(changedFields).toContain("memory");
});
it("does not create a revision when only non-config fields change", async () => {

View File

@@ -102,6 +102,8 @@ interface AgentData {
lastError?: string;
instructionsPath?: string;
instructionsText?: string;
soul?: string;
memory?: string;
bundleConfig?: InstructionsBundleConfig;
}
interface AgentLock {
@@ -173,6 +175,8 @@ export class AgentStore extends EventEmitter {
...(input.permissions && { permissions: input.permissions }),
...(input.instructionsPath && { instructionsPath: input.instructionsPath }),
...(input.instructionsText && { instructionsText: input.instructionsText }),
...(input.soul && { soul: input.soul }),
...(input.memory && { memory: input.memory }),
...(input.bundleConfig && { bundleConfig: input.bundleConfig }),
};
@@ -628,6 +632,8 @@ export class AgentStore extends EventEmitter {
...("totalOutputTokens" in updates && { totalOutputTokens: updates.totalOutputTokens }),
...("instructionsPath" in updates && { instructionsPath: updates.instructionsPath }),
...("instructionsText" in updates && { instructionsText: updates.instructionsText }),
...(updates.soul !== undefined && { soul: updates.soul }),
...(updates.memory !== undefined && { memory: updates.memory }),
...("bundleConfig" in updates && { bundleConfig: updates.bundleConfig }),
};
@@ -1544,6 +1550,8 @@ export class AgentStore extends EventEmitter {
| "permissions"
| "instructionsPath"
| "instructionsText"
| "soul"
| "memory"
| "bundleConfig"
| "metadata"
> {
@@ -1557,6 +1565,8 @@ export class AgentStore extends EventEmitter {
permissions: snapshot.permissions ? { ...snapshot.permissions } : undefined,
instructionsPath: snapshot.instructionsPath,
instructionsText: snapshot.instructionsText,
soul: snapshot.soul,
memory: snapshot.memory,
bundleConfig: snapshot.bundleConfig
? {
...snapshot.bundleConfig,
@@ -1756,6 +1766,8 @@ export class AgentStore extends EventEmitter {
lastError: data.lastError,
instructionsPath: data.instructionsPath,
instructionsText: data.instructionsText,
soul: data.soul,
memory: data.memory,
bundleConfig: data.bundleConfig,
};
}
@@ -1783,6 +1795,8 @@ export class AgentStore extends EventEmitter {
lastError: agent.lastError,
instructionsPath: agent.instructionsPath,
instructionsText: agent.instructionsText,
soul: agent.soul,
memory: agent.memory,
bundleConfig: agent.bundleConfig,
};

View File

@@ -1818,6 +1818,10 @@ export interface Agent {
instructionsPath?: string;
/** Inline custom instructions appended to the agent's system prompt at execution time. Max 50,000 chars. */
instructionsText?: string;
/** Agent personality/identity description — defines the agent's character, tone, and behavioral traits. Max 10,000 chars. */
soul?: string;
/** Per-agent accumulated knowledge — stores learnings, preferences, and context the agent has gathered. Max 50,000 chars. */
memory?: string;
/** Structured instruction bundle configuration for managed/external markdown files. */
bundleConfig?: InstructionsBundleConfig;
}
@@ -1919,6 +1923,8 @@ export interface AgentCreateInput {
permissions?: Record<string, boolean>;
instructionsPath?: string;
instructionsText?: string;
soul?: string;
memory?: string;
bundleConfig?: InstructionsBundleConfig;
}
@@ -1938,6 +1944,8 @@ export interface AgentUpdateInput {
totalOutputTokens?: number;
instructionsPath?: string;
instructionsText?: string;
soul?: string;
memory?: string;
bundleConfig?: InstructionsBundleConfig;
}
@@ -2028,6 +2036,8 @@ export interface AgentConfigSnapshot {
permissions?: Record<string, boolean>;
instructionsPath?: string;
instructionsText?: string;
soul?: string;
memory?: string;
bundleConfig?: InstructionsBundleConfig;
metadata: Record<string, unknown>;
}
@@ -2073,6 +2083,8 @@ export function agentToConfigSnapshot(agent: Agent): AgentConfigSnapshot {
permissions: agent.permissions ? { ...agent.permissions } : undefined,
instructionsPath: agent.instructionsPath,
instructionsText: agent.instructionsText,
soul: agent.soul,
memory: agent.memory,
bundleConfig: agent.bundleConfig
? {
...agent.bundleConfig,
@@ -2098,6 +2110,8 @@ export function diffConfigSnapshots(
"permissions",
"instructionsPath",
"instructionsText",
"soul",
"memory",
"bundleConfig",
"metadata",
];