update agents skill design
This commit is contained in:
137
src/lib/agents/useAgents.js
Normal file
137
src/lib/agents/useAgents.js
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user