From 55ddaa134a63b177fa4b195ef175c694f88fb0ac Mon Sep 17 00:00:00 2001 From: Aravind Date: Fri, 18 Sep 2026 15:25:35 +0530 Subject: [PATCH] =?UTF-8?q?refactor(ts-migration):=20Phase=2011=20batch=20?= =?UTF-8?q?4=20=E2=80=94=20the=20agent=20layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8 --- .../agents/{agentConfig.js => agentConfig.ts} | 0 .../agents/{agentFields.js => agentFields.ts} | 6 +-- .../{agentLifecycle.js => agentLifecycle.ts} | 4 +- .../agents/{agentStore.js => agentStore.ts} | 6 ++- .../{capabilityTest.js => capabilityTest.ts} | 0 src/lib/agents/{context.js => context.ts} | 0 ...ionInsights.js => conversationInsights.ts} | 0 .../{customAgents.js => customAgents.ts} | 0 src/lib/agents/{knowledge.js => knowledge.ts} | 0 src/lib/agents/{registry.js => registry.ts} | 0 src/lib/agents/{runtime.js => runtime.ts} | 1 + src/lib/agents/{useAgents.js => useAgents.ts} | 43 ++++++++++++++++--- .../agents/{vocabulary.js => vocabulary.ts} | 0 13 files changed, 48 insertions(+), 12 deletions(-) rename src/lib/agents/{agentConfig.js => agentConfig.ts} (100%) rename src/lib/agents/{agentFields.js => agentFields.ts} (98%) rename src/lib/agents/{agentLifecycle.js => agentLifecycle.ts} (98%) rename src/lib/agents/{agentStore.js => agentStore.ts} (98%) rename src/lib/agents/{capabilityTest.js => capabilityTest.ts} (100%) rename src/lib/agents/{context.js => context.ts} (100%) rename src/lib/agents/{conversationInsights.js => conversationInsights.ts} (100%) rename src/lib/agents/{customAgents.js => customAgents.ts} (100%) rename src/lib/agents/{knowledge.js => knowledge.ts} (100%) rename src/lib/agents/{registry.js => registry.ts} (100%) rename src/lib/agents/{runtime.js => runtime.ts} (99%) rename src/lib/agents/{useAgents.js => useAgents.ts} (80%) rename src/lib/agents/{vocabulary.js => vocabulary.ts} (100%) diff --git a/src/lib/agents/agentConfig.js b/src/lib/agents/agentConfig.ts similarity index 100% rename from src/lib/agents/agentConfig.js rename to src/lib/agents/agentConfig.ts diff --git a/src/lib/agents/agentFields.js b/src/lib/agents/agentFields.ts similarity index 98% rename from src/lib/agents/agentFields.js rename to src/lib/agents/agentFields.ts index a19ca05..51a212b 100644 --- a/src/lib/agents/agentFields.js +++ b/src/lib/agents/agentFields.ts @@ -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; diff --git a/src/lib/agents/agentLifecycle.js b/src/lib/agents/agentLifecycle.ts similarity index 98% rename from src/lib/agents/agentLifecycle.js rename to src/lib/agents/agentLifecycle.ts index 1bcb0ce..75e7352 100644 --- a/src/lib/agents/agentLifecycle.js +++ b/src/lib/agents/agentLifecycle.ts @@ -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`; diff --git a/src/lib/agents/agentStore.js b/src/lib/agents/agentStore.ts similarity index 98% rename from src/lib/agents/agentStore.js rename to src/lib/agents/agentStore.ts index dd1d0c0..22e3842 100644 --- a/src/lib/agents/agentStore.js +++ b/src/lib/agents/agentStore.ts @@ -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 }; diff --git a/src/lib/agents/capabilityTest.js b/src/lib/agents/capabilityTest.ts similarity index 100% rename from src/lib/agents/capabilityTest.js rename to src/lib/agents/capabilityTest.ts diff --git a/src/lib/agents/context.js b/src/lib/agents/context.ts similarity index 100% rename from src/lib/agents/context.js rename to src/lib/agents/context.ts diff --git a/src/lib/agents/conversationInsights.js b/src/lib/agents/conversationInsights.ts similarity index 100% rename from src/lib/agents/conversationInsights.js rename to src/lib/agents/conversationInsights.ts diff --git a/src/lib/agents/customAgents.js b/src/lib/agents/customAgents.ts similarity index 100% rename from src/lib/agents/customAgents.js rename to src/lib/agents/customAgents.ts diff --git a/src/lib/agents/knowledge.js b/src/lib/agents/knowledge.ts similarity index 100% rename from src/lib/agents/knowledge.js rename to src/lib/agents/knowledge.ts diff --git a/src/lib/agents/registry.js b/src/lib/agents/registry.ts similarity index 100% rename from src/lib/agents/registry.js rename to src/lib/agents/registry.ts diff --git a/src/lib/agents/runtime.js b/src/lib/agents/runtime.ts similarity index 99% rename from src/lib/agents/runtime.js rename to src/lib/agents/runtime.ts index 0947b95..ed021d1 100644 --- a/src/lib/agents/runtime.js +++ b/src/lib/agents/runtime.ts @@ -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 || [])]; diff --git a/src/lib/agents/useAgents.js b/src/lib/agents/useAgents.ts similarity index 80% rename from src/lib/agents/useAgents.js rename to src/lib/agents/useAgents.ts index 9274f92..bf119f0 100644 --- a/src/lib/agents/useAgents.js +++ b/src/lib/agents/useAgents.ts @@ -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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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) }); diff --git a/src/lib/agents/vocabulary.js b/src/lib/agents/vocabulary.ts similarity index 100% rename from src/lib/agents/vocabulary.js rename to src/lib/agents/vocabulary.ts