update agents skill design

This commit is contained in:
2026-08-20 18:18:10 +05:30
parent 161b237695
commit b2e6868824
75 changed files with 14586 additions and 98 deletions

137
src/lib/agents/useAgents.js Normal file
View File

@@ -0,0 +1,137 @@
import { useCallback, useMemo } from 'react';
import { usePreferences, useUpdatePreferences } from '@/lib/krowHooks';
import { reportSave } from '@/lib/skills/saveFeedback';
import { AGENTS, parseAgent, readAgentRegistry, validateAgentSource } from './registry';
import { customAgentSource, removeCustomAgent, upsertCustomAgent } from './customAgents';
import { archiveAgent, duplicateAgent, publishAgent, restoreAgent } from './agentLifecycle';
/**
* Reading and writing agents, in one place.
*
* Every screen that changes an agent goes through here. That is deliberate: the
* list can publish, the detail page can publish, and two implementations of
* "publish" would eventually disagree about what publishing means.
*
* Storage is the account's preferences, holding **Markdown** — the same
* artefact a file in `src/agents/` is. The forms never expose it: a person
* fills in fields, `agentPatch` writes them into frontmatter, and the parser
* reads them back. Markdown stays the definition format without ever being
* something an HR user has to see.
*
* Editing a shipped agent writes an account definition of the same id, which
* the registry reports as `shadowed`. That is the existing override mechanism,
* not a new one — a shipped agent is never mutated on disk.
*/
export function useAgents() {
const preferences = usePreferences();
const updatePreferences = useUpdatePreferences();
const stored = useMemo(() => preferences.customAgents || [], [preferences.customAgents]);
const customSkills = useMemo(() => preferences.customSkills || [], [preferences.customSkills]);
const { agents, diagnostics } = useMemo(
() => readAgentRegistry(stored, { customSkills }),
[stored, customSkills]
);
/** Whether this id is shipped with the product rather than authored here. */
const isShipped = useCallback((id) => AGENTS.some((a) => a.id === id), []);
/** Whether the account has its own definition for this id. */
const isOverridden = useCallback((id) => customAgentSource(stored, id) !== null, [stored]);
/**
* The Markdown behind an agent.
*
* The account's own copy when there is one, otherwise the shipped definition —
* so editing a shipped agent starts from what it actually says rather than
* from a blank.
*/
const sourceFor = useCallback(
(id) => customAgentSource(stored, id) || AGENTS.find((a) => a.id === id)?.markdown || null,
[stored]
);
/**
* Writes a definition.
*
* Validated first and refused with a message rather than stored broken: a
* definition that cannot be read is an agent that silently stops existing.
* Returns `{ ok, error }` so a form can stay on screen and say why.
*/
const save = useCallback(async (/** @type {string} */ source, /** @type {any} */ { message } = {}) => {
const problem = validateAgentSource(source);
if (problem) return { ok: false, error: problem };
const { agent, next } = upsertCustomAgent(stored, source);
await updatePreferences.mutateAsync(
{ customAgents: next },
reportSave(message || `${agent.name} saved`)
);
return { ok: true, agent };
}, [stored, updatePreferences]);
/** Removes the account's definition. A shipped agent returns to its shipped form. */
const remove = useCallback(async (id) => {
const next = removeCustomAgent(stored, id);
await updatePreferences.mutateAsync(
{ customAgents: next },
reportSave(isShipped(id) ? 'Reverted to the shipped definition' : 'Agent removed')
);
return { ok: true };
}, [stored, updatePreferences, isShipped]);
/**
* Publishes, refusing to overwrite a newer published version.
*
* Returns `{ ok: false, conflict }` when the stored definition has moved on,
* so the screen can say what would be lost instead of losing it.
*/
const publish = useCallback(async (id) => {
const source = sourceFor(id);
if (!source) return { ok: false, error: 'That agent has no definition to publish.' };
const live = agents.find((a) => a.id === id);
const result = publishAgent(source, {
publishedVersion: live?.status === 'published' ? live.version : 0,
});
if (result.conflict) return { ok: false, conflict: result.conflict };
return save(result.source, { message: `${live?.name || id} published` });
}, [sourceFor, agents, save]);
const archive = useCallback(async (id) => {
const source = sourceFor(id);
if (!source) return { ok: false, error: 'That agent has no definition to archive.' };
return save(archiveAgent(source), { message: `${agents.find((a) => a.id === id)?.name || id} archived` });
}, [sourceFor, agents, save]);
const restore = useCallback(async (id) => {
const source = sourceFor(id);
if (!source) return { ok: false, error: 'That agent has no definition to restore.' };
return save(restoreAgent(source), { message: 'Restored as a draft' });
}, [sourceFor, save]);
const duplicate = useCallback(async (id) => {
const source = sourceFor(id);
if (!source) return { ok: false, error: 'That agent has no definition to copy.' };
const copy = duplicateAgent(source, { existingIds: agents.map((a) => a.id) });
const result = await save(copy, { message: 'Copy created as a draft' });
return result.ok ? { ...result, id: parseAgent(copy, { custom: true }).id } : result;
}, [sourceFor, agents, save]);
return {
agents,
diagnostics,
saving: updatePreferences.isPending,
isShipped,
isOverridden,
sourceFor,
save,
remove,
publish,
archive,
restore,
duplicate,
};
}