refactor(ts-migration): Phase 11 batch 4 — the agent layer
All thirteen modules under `src/lib/agents/`. Renamed and annotated; no
logic touched. 13/13 erase to byte-identical JavaScript, and the bundle
still hashes to 74d17e2d…
Project typecheck errors are now 9, down from the 20 this phase started
from. Nothing was suppressed: batch 3's `Frontmatter` cleared 4 and this
batch's annotations cleared 7 more that had been sitting in `agentStore`,
`runtime` and `useAgents` since before the migration.
Two findings, both recorded rather than fixed:
`agentSkillIds(agent)` takes one parameter and is called with two, at
`runtime.ts:83` and `:95`. Not a bug — its own doc comment says so:
subagent skills were deliberately removed from it, because CLAUDE.md §3
makes `subagents` a delegation list rather than a skill list, and
"`agents` is still accepted so every call site keeps working; it is no
longer read." The contract is restored with an overload signature, which
emits no JavaScript — confirmed by reading the emitted output, where
`agentSkillIds` still takes exactly one parameter. Worth knowing that
`agentScopedDisabledWith` therefore computes what `agentScopedDisabled`
computes; that is intended, and `skill-check.mjs` asserts the behaviour
at eleven call sites.
`useAgents` returns five different shapes from eleven `return`
statements, which is the latent problem the migration plan predicted
here. `AgentActionResult` writes them down, but open: `ok` plus four
optional fields. Nothing stops a caller reading `.agent` off a failure
and getting `undefined` — `AgentDetail.jsx` reads `.conflict` and
`.error` off the same value. Closing it properly needs `as const` on
eleven literals so `ok` stops widening to `boolean` and starts
discriminating, which is an edit to these function bodies and not
something a rename may do. The type is the record of the decision, not
the decision.
Writing that interface also corrected my own count. I described four
shapes; the compiler rejected `duplicate`'s `{ ...result, id }` and made
it five.
Other annotations: React Query v5 infers `void` for an unconstrained
`mutationFn` parameter, so both mutations in `agentStore` had their
variables stated; `existingIds = []` in `agentLifecycle` infers
`undefined[]`, which rejects `.includes(id)`, so it is `string[]`; two
accumulators and two inline JSDoc hatches restated as annotations.
Measured against `fc8d7ee`:
typecheck 9 errors, down from 16; no new error anywhere
lint exit 0, 0 errors, 289 warnings
npm test 1684/1691, the same 7 failures verbatim
build exit 0, identical bundle hash
type erasure 46/46 byte-identical across Phase 11 so far
No baseline artifact touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
@@ -90,8 +90,8 @@ const listOrRemove = (list) => (list && list.length ? list : REMOVE);
|
||||
* only knows about four fields cannot erase the other ten, and a definition
|
||||
* hand-written with comments and key order survives being saved from the form.
|
||||
*/
|
||||
export function agentPatch(fields = {}) {
|
||||
const patch = {};
|
||||
export function agentPatch(fields: any = {}) {
|
||||
const patch: any = {};
|
||||
|
||||
const scalar = (key, value) => {
|
||||
if (value === undefined) return;
|
||||
@@ -198,7 +198,7 @@ function writeSection(body, heading, text) {
|
||||
* therefore never has to remember which half a given field lives in — which is
|
||||
* exactly the mistake that made instructions silently unsaveable.
|
||||
*/
|
||||
export function applyAgentFields(source, fields = {}) {
|
||||
export function applyAgentFields(source, fields: any = {}) {
|
||||
const patched = patchFrontmatter(source, agentPatch(fields));
|
||||
if (fields.instructions === undefined) return patched;
|
||||
|
||||
@@ -63,7 +63,9 @@ export const restoreAgent = (source) =>
|
||||
* copy into the switcher the moment it was made.
|
||||
*/
|
||||
/** @param {string} source @param {any} [options] */
|
||||
export function duplicateAgent(source, { name, existingIds = [] } = {}) {
|
||||
export function duplicateAgent(source, { name, existingIds = [] }: {
|
||||
name?: any; existingIds?: string[];
|
||||
} = {}) {
|
||||
const agent = parseAgent(source, { custom: true });
|
||||
const copyName = name || `${agent.name} copy`;
|
||||
|
||||
@@ -67,7 +67,9 @@ export function sourcesFrom(rows, shippedIds = new Set()) {
|
||||
export function useSaveAgentDefinition() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ definitionId, markdown, visibility = 'personal' }) => {
|
||||
mutationFn: async ({ definitionId, markdown, visibility = 'personal' }: {
|
||||
definitionId: string; markdown: string; visibility?: string;
|
||||
}) => {
|
||||
const rows = qc.getQueryData(KEY) || (await base44.entities.AgentDefinition.list('-created_date', 200));
|
||||
const existing = (rows || []).find((r) => r.definition_id === definitionId);
|
||||
/* visibility is immutable after creation, so it is sent only on create —
|
||||
@@ -85,7 +87,7 @@ export function useSaveAgentDefinition() {
|
||||
export function useDeleteAgentDefinition() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (definitionId) => {
|
||||
mutationFn: async (definitionId: string) => {
|
||||
const rows = qc.getQueryData(KEY) || (await base44.entities.AgentDefinition.list('-created_date', 200));
|
||||
const existing = (rows || []).find((r) => r.definition_id === definitionId);
|
||||
if (!existing) return { ok: true };
|
||||
@@ -61,6 +61,7 @@ import { reasoningFor } from './vocabulary';
|
||||
* read. Removing the parameter would be a wider edit for no behavioural gain,
|
||||
* and agentScopedDisabledWith exists precisely to pass it.
|
||||
*/
|
||||
export function agentSkillIds(agent: any, agents?: any): any[];
|
||||
export function agentSkillIds(agent) {
|
||||
if (!agent) return [];
|
||||
return [...new Set(agent.skills || [])];
|
||||
@@ -9,6 +9,37 @@ import { AGENTS, parseAgent, readAgentRegistry, validateAgentSource } from './re
|
||||
import { customAgentSource } from './customAgents';
|
||||
import { archiveAgent, duplicateAgent, publishAgent, restoreAgent } from './agentLifecycle';
|
||||
|
||||
/**
|
||||
* What every action on this hook resolves to.
|
||||
*
|
||||
* Five shapes across eleven `return` statements: `{ ok: false, error }`,
|
||||
* `{ ok: false, conflict }`, `{ ok: true, agent }`, a bare `{ ok: true }`, and
|
||||
* `duplicate`'s `{ ...result, id }`, which spreads a success and adds the id of
|
||||
* the copy it made.
|
||||
*
|
||||
* This is written open — `ok` plus three optional fields — rather than as the
|
||||
* discriminated union it wants to be, and that is a deliberate limit of this
|
||||
* phase rather than a judgement that the open version is right.
|
||||
*
|
||||
* A proper union would make `result.agent` unreadable until `result.ok` had
|
||||
* been checked, and `result.conflict` unreadable on a success. As written,
|
||||
* nothing stops a caller reading `.agent` off a failure and getting
|
||||
* `undefined`. Closing it needs `as const` on ten literals so `ok` stops
|
||||
* widening to `boolean` and starts discriminating — edits to the bodies of
|
||||
* these functions, which a rename is not allowed to make.
|
||||
*
|
||||
* Recorded here so the follow-up is a decision someone takes, not something
|
||||
* that has to be rediscovered.
|
||||
*/
|
||||
export interface AgentActionResult {
|
||||
ok: boolean;
|
||||
error?: any;
|
||||
conflict?: any;
|
||||
agent?: any;
|
||||
/** Only `duplicate` sets this: the id of the copy. */
|
||||
id?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reading and writing agents, in one place.
|
||||
*
|
||||
@@ -91,7 +122,7 @@ export function useAgents() {
|
||||
* 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 save = useCallback(async (source: string, { message }: any = {}): Promise<AgentActionResult> => {
|
||||
const problem = validateAgentSource(source);
|
||||
if (problem) return { ok: false, error: problem };
|
||||
|
||||
@@ -109,7 +140,7 @@ export function useAgents() {
|
||||
}, [saveDefinition]);
|
||||
|
||||
/** Removes the account's definition. A shipped agent returns to its shipped form. */
|
||||
const remove = useCallback(async (id) => {
|
||||
const remove = useCallback(async (id): Promise<AgentActionResult> => {
|
||||
try {
|
||||
await deleteDefinition.mutateAsync(id);
|
||||
} catch (error) {
|
||||
@@ -125,7 +156,7 @@ export function useAgents() {
|
||||
* 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 publish = useCallback(async (id): Promise<AgentActionResult> => {
|
||||
const source = sourceFor(id);
|
||||
if (!source) return { ok: false, error: 'That agent has no definition to publish.' };
|
||||
|
||||
@@ -138,19 +169,19 @@ export function useAgents() {
|
||||
return save(result.source, { message: `${live?.name || id} published` });
|
||||
}, [sourceFor, agents, save]);
|
||||
|
||||
const archive = useCallback(async (id) => {
|
||||
const archive = useCallback(async (id): Promise<AgentActionResult> => {
|
||||
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 restore = useCallback(async (id): Promise<AgentActionResult> => {
|
||||
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 duplicate = useCallback(async (id): Promise<AgentActionResult> => {
|
||||
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) });
|
||||
Reference in New Issue
Block a user